进程
进程是对正在运行程序的一个抽象。一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。从概念上说,每个进程拥有它自己的虚拟 CPU(实际真正的CPU在个进程之间来回切换)。各个进程有自己的内存空间、数据栈等,所以只能使用进程间通讯(interprocess communication,IPC),而不能直接共享信息。
进程是系统进行资源分配和调度的一个独立单位。
多进程
最直观的就是一个个pid,官方的说法就:进程是程序在计算机上的一次执行活动。一次main函数执行就是一个进程。
linux下创建子进程的调用是fork()。fork的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程,新进程称为子进程。
fork函数“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id(从根本上说fork是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
参考代码:
#include <sys/types.h> #include <unistd.h> pid_t fork(void); #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> void print_exit() { printf("The exit pid:%d\n",getpid()); } main() { pid_t pid; pid=fork(); //创建新进程 atexit(print_exit); //注册该进程退出时的回调函数 if(pid<0) printf("Error in fork!"); else if (pid==0) //返回0,表示在子进程中运行 { printf("The return pid = %d\n",pid); printf("I am the child process,my process id is %d\n", getpid()); } else //返回子进程的pid,表示在父进程中运行 { printf("The return pid = %d\n",pid); printf("I am the parent process,my process id is %d\n", getpid()); sleep(2); wait(); } }
wait和waitpid函数:
当进程退出时,向其父进程发送一个SIGCHLD信号,默认情况下总是忽略该信号,此时进程的状态一直保留在内存中,直到父进程调用wait函数手机状态信息,才会对它进行清理,父进程尚未调用wait或waitpid对它进行清理前,该进程状态称为僵尸进程。
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。
进程间通信
在进行进程切换时,都要依赖于内核中的进程调度。
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
主要有以下几种方式:
管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
管道是一种最基本的IPC机制,由pipe函数创建:
#include <unistd.h> int pipe(int filedes[2]);
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数signal外,还支持语义符合Posix.1标准的信号函数sigaction。
报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
线程
进程—独立分配资源的单位
线程—调度和分配的基本单位
p.s.进程的一个实体,基本不拥有资源,只有一点运行中必不可少的资源(如程序计数器、一组寄存器和栈),它可与同属一个进程的其他线程共享进程拥有的全部资源。
线程提高系统内程序并发执行的程度,从而进一步提高系统的吞吐量
p.s.每个进程有一个地址空间和一个控制线程,所以无法实现“在同一个地址空间中准并行运行多个控制线程的情形”。多线程加入了一种新的元素——并行实体共享同一个地址空间和所有可用数据的能力。这是多进程模型(它们具有不同的地址空间)所无法表达的。
线程比进程轻量级,所以更容易创建和撤销。
若多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是若存在大量的计算和大量的IO处理,拥有多个线程允许这些活动彼此重叠进行,加快应用程序执行的速度。
多线程
线程ID类型:phtread_t
包含头文件:#include<pthread.h>
在Linux上线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项
创建线程函数:
int pthread_create(pthread_t *restrict tidp, //线程ID const pthread_attr_t *restrict attr,//线程属性 void *(*start_routine)(void*),//新创建线程从start_routine开始执行 void *restrict arg);//执行函数的参数
返回值:成功-0;失败-返回错误编号(strerror(全局变量errno)得到错误信息)
获得线程ID函数:
pthread_t pthread_self(void);
线程Joining Threads函数:
int pthread_join(pthread_t thread, void **value_ptr);
若不存在pthread_join,主线程(main函数中)会很快return,从而导致整个进程结束,那么新创建的线程还没有执行结束。
调用该函数的线程将挂起等待,直到id为thread的线程终止,由value_ptr指向返回值,若对线程终止状态不关心,传NULL给参数value_ptr。
p.s.如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3. 线程可以调用pthread_exit终止自己。
线程终止函数:
void pthread_exit(void *value_ptr);
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
int pthread_cancel(pthread_t thread);
异步终结就是当其他线程调用pthread_cancel的时候,线程就立刻被结束。
同步终结则不会立刻终结,它会继续运行,直到到达下一个结束点(cancellation point)。
当一个线程被按照默认的创建方式创建,那么它的属性是同步终结。
线程实现:
在Linux中,新建的线程并不是在原先的进程中,而是系统通过 一个系统调用clone()。该系统copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。这样,原先进程中的变量变动在copy后的进程中便能体现出来。
参考代码:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> int num=0; void *add(void *arg) { //线程执行函数,执行500次加法 int i = 0,tmp; for (; i <500; i++) { tmp=num+1; num=tmp; printf("add+1,result is:%d\n",num); } return ((void *)0); } void *sub(void *arg) { //线程执行函数,执行500次减法 int i =0,tmp; for(;i<500;i++) { tmp=num-1; num=tmp; printf("sub-1,result is:%d\n",num); } return ((void *)0); } int main(int argc, char** argv) { pthread_t tid1,tid2; int err; void *tret; //创建线程,返回线程号给tid1,线程函数入口add,参数NULL err=pthread_create(&tid1,NULL,add,NULL); if(err!=0) { printf("pthread_create error:%s\n",strerror(err)); exit(-1); } err=pthread_create(&tid2,NULL,sub,NULL); if(err!=0) { printf("pthread_create error:%s\n",strerror(err)); exit(-1); } //阻塞等待线程id为tid1的线程,直到该线程退出,返回值赋给tret err=pthread_join(tid1,&tret); if(err!=0) { printf("can not join with thread1:%s\n",strerror(err)); exit(-1); } printf("After pthread1:return value = %d, pthread1 exit code = %d\n", err,(int)tret); err=pthread_join(tid2,&tret); if(err!=0) { printf("can not join with thread2:%s\n",strerror(err)); exit(-1); } printf("After pthread2:return value = %d, pthread2 exit code = %d\n", err,(int)tret); return 0; }
线程间同步
A. 互斥量(mutex)——数据类型pthread_mutex_t
获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex初始化:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex);
如果Mutex变量是静态分配的(全局变量或static变量),可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。
pthread_mutex_t mylock=PTHREAD_MUTEX_INITIALIZER;
Mutex加解锁操作:
int pthread_mutex_lock (pthread_mutex_t *__mutex); int pthread_mutex_unlock (pthread_mutex_t *__mutex);
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
int pthread_mutex_trylock(pthread_mutex_t *__mutex);
B. 读写锁Reader-Writer Lock——允许多个线程同时读,只能有一个线程同时写
Reader-Writer Lock比Mutex具有更好的并发性, 适用于读的次数远大于写的情况。
读写锁初始化:
int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock, __const pthread_rwlockattr_t *__restrict, __attr); int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock);
读加锁:
int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock);
写加锁:
int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock);
解锁:
int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);
C. 条件变量(Condition Variable)——数据类型pthread_cond_t
线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。
在pthread库中通过条件变量来阻塞等待一个条件,或者唤醒等待这个条件的线程。
条件变量初始化:
int pthread_cond_init (pthread_cond_t *__restrict __cond, __const pthread_condattr_t *__restrict, __cond_attr); int pthread_cond_destroy (pthread_cond_t *__cond); pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
条件等待,使用pthread_cond_wait等待条件为真:
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime) int pthread_cond_wait (pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex)
唤醒线程:
int pthread_cond_signal (pthread_cond_t *__cond); //唤醒等待该条件的某个线程 int pthread_cond_broadcast (pthread_cond_t *__cond) //唤醒等待该条件的所有线程
p.s.一个Condition Variable总是和一个Mutex搭配使用的。
参考代码:生产者-消费者:LIFO
#include <stdlib.h> #include <pthread.h> #include <stdio.h> /*LIFO*/ struct msg{ struct msg *next; int num; }; struct msg *head; pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void *consumer(void *p) { struct msg *mp; for(;;) { pthread_mutex_lock(&lock); while(head == NULL) pthread_cond_wait(&has_product,&lock); mp = head; head = mp->next; pthread_mutex_unlock(&lock); printf("Consume %d\n",mp->num); free(mp); sleep(rand()%5); } } void *producer(void *p) { struct msg *mp; for(;;) { mp = malloc(sizeof(struct msg)); mp->num = rand() % 1000 + 1; printf("Produce %d\n",mp->num); pthread_mutex_lock(&lock); mp->next = head; head = mp; pthread_mutex_unlock(&lock); pthread_cond_signal(&has_product); sleep(rand()%5); } } int main(int argc,char *argv[]) { pthread_t pid,cid; srand(time(NULL)); pthread_create(&pid,NULL,producer,NULL); pthread_create(&cid,NULL,consumer,NULL); pthread_join(pid,NULL); pthread_join(cid,NULL); return 0; }
D. 信号量Semaphore
信号量(Semaphore)和Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1。这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步。
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem); int sem_post(sem_t * sem); int sem_destroy(sem_t * sem);
调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()。调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。
Processes Vs. Threads
感觉linux的进程跟线程界限不是那么清晰。不过还没写过linux下的多线程,给的例子很不错