在深入理解Linux下多进程编程之前,我们首先要了解Linux下进程在运行时的内存布局。 Linux 进程内存管理的对象都是虚拟内存,每个进程先天就有 0-4G 的各自互不干涉的虚拟内存空间,0—3G 是用户空间执行用户自己的代码, 高 1GB 的空间是内核空间执行 Linu x 系统调用,这里存放在整个内核的代码和所有的内核模块,用户所看到和接触的都是该虚拟地址,并不是实际的物理内存地址。 Linux下一个进程在内存里有三部分的数据,就是”代码段”、”堆栈段”和"数据段”。如下图:

Linux下有两个基本的系统调用可以用于创建子进程:fork()和vfork()。
我们着重介绍fork()。fork在英文中是"分叉"的意思。一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就”分叉”了,所以这个名字取得很形象。在我们编程的过程中,一个函数调用只有一次返回(return),但由于fork()系统调用会创建一个新的进程,这时它会有两次返回。一次返回是给父进程,其返回值是子进程的PID(Process ID),第二次返回是给子进程,其返回值为0。所以我们在调用fork()后,需要通过其返回值来判断当前的代码是在父进程还是子进程运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明是父进程在运行,而如果返回值<0的话,说明fork()系统调用出错。fork 函数调用失败的原因主要有两个:
每个子进程只有一个父进程,并且每个进程都可以通过getpid()获取自己的进程PID,也可以通过getppid()获取父进程的PID,这样在fork()时返回0给子进程是可取的。一个进程可以创建多个子进程,这样对于父进程而言,他并没有一个API函数可以获取其子进程的进程ID,所以父进程在通过fork()创建子进程的时候,必须通过返回值的形式告诉父进程其创建的子进程PID。这也是fork()系统调用两次返回值设计的原因。
接下来我们写一段程序来了解一下fork()函数创建进程的用法:
#include
#include
#include
#include
#include #define buf "Hello, nice to meet you!\n"
int a = 1;
int main(int argc, char argv[])
{pid_t pid;int b = 11;if (write(STDOUT_FILENO, buf, sizeof(buf)) < 0){printf("Write string to STDOUT_FILENO errer:%s\n", strerror(errno));return -1;}printf("Before fork\n");pid = fork();if (pid < 0){printf("Fork() create child process failure:%s\n", strerror(errno));return -1;}else if (0 == pid){printf("Child process PID[%d] start running. My parent PID is [%d]\n", getpid(), getppid());a++;b++;}else{printf("Parent process PID[%d] continue running, and my son PID is [%d]\n", getpid(), pid);}/*父进程和子进程都会运行到return 0,所以下面的语句会打印两次哦*/printf("PID = %d, a = %d, b = %d\n", getpid(), a, b);return 0;
}
直接运行结果:
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao$ ./fork1
Hello, nice to meet you!
Before fork
Parent process PID[3683] continue running, and my son PID is [3684]
PID = 3683, a = 1, b = 11
Child process PID[3684] start running. My parent PID is [3683]
PID = 3684, a = 2, b = 12
fork()系统调用会创建一个新的子进程,这个子进程是父进程的一个副本。这也意味着,系统在创建新的子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间,子进程对这些内存的修改并不会影响父进程空间的相应内存。就像上面的程序一样,我们在子进程中改变了a和b的值,但是父进程中并没有改变。这时系统中出现两个基本完全相同的进程(父、子进程),这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
但是我们输出重定向到一个文件中的结果:
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao$ ./fork1 > a.log
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao$ cat a.log
Hello, nice to meet you!
Before fork
Parent process PID[3688] continue running, and my son PID is [3689]
PID = 3688, a = 1, b = 11
Before fork
Child process PID[3689] start running. My parent PID is [3688]
PID = 3689, a = 2, b = 12
结果就不一样了,Befere fork打印了两次,这是为什么呢?大家可以去了解一下行缓冲和全缓冲的概念,这篇文章:行缓冲、全缓冲、无缓冲以及用户缓冲区、内核缓冲区介绍
给大家解释一下:printf()库函数在标准输出是终端时默认是行缓冲,所以遇见‘\n’就会输出;而当标准输出重定向到文件中后该函数是全缓冲的;所以第一次的结果就是遇见‘\n’输出Before fork。但是我们重定向到文件中就是全缓冲了,全缓冲到缓冲区的大小4k才会输出,显然没有到达,所以就留在缓冲区了,但是进程结束就会刷新缓冲区将缓冲区的内容输出,所以子进程结束输出一次,父进程结束输出一次。
那为什么’Hello, nice to meet you!‘只打印了一次呢?因为write()是无缓冲的,直接输出。
可以理解为第一次缓冲区中没有数据,已经输出了,所以只有一次;第二次父子进程的缓冲区中都有‘Before fork’,所以输出两次。
从上面的例子中我们可以知道,知道子进程从父进程那里继承什么或未继承什么将有助于我们今后的编程。
子进程自父进程继承到:
子进程自己独有:
我们在服务器进行多进程编程的时候,我们需要注意一点,因为子进程可以继承父进程的文件描述符,所以我们才能这样完成多进程服务器编程的,这是最基本的,不然没法实现的。
在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。
一个进程创建后,会首先生成一个缺省的线程,通常称这个线程为主线程(或称控制线程),C程序中,主线程就是通过main函数进入的线程,由主线程调用 pthread_create() 创建的线程称为子线程,子线程也可以有自己的入口函数,该函数由用户在创建的时候指定。每个线程都有自己的线程ID,可以通过 pthread_self() 函数获取。最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。
主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void*arg);
说明: pthreand_create()用来创建一个线程,并执行第三个参数start_routine所指向的函数。
typedef struct
{int detachstate; 线程的分离状态int schedpolicy; 线程调度策略struct sched_param schedparam; 线程的调度参数int inheritsched; 线程的继承性int scope; 线程的作用域size_t guardsize; 线程栈末尾的警戒缓冲区大小int stackaddr_set;void * stackaddr; 线程栈的位置size_t stacksize; 线程栈的大小
}pthread_attr_t;
线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:
线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
创建线程时一般会将线程设置为分离状态,具体有两种方法:
下面代码中有涉及到分离和非分离两种状态,注释中都有注解。
#include
#include
#include
#include
#include void *thread_worker1(void *args);
void *thread_worker2(void *args);int main(int argc, char *argv[])
{int shared_var = 1000;pthread_t tid; //线程IDpthread_attr_t thread_attr; //创建线程属性变量/*调用pthread_attr_init 函数初始化线程属性*/if(pthread_attr_init(&thread_attr)){printf("pthread_attr_init() failure: %s\n",strerror(errno));return -1;}/*设置线程的栈大小为120K*/if(pthread_attr_setstacksize(&thread_attr, 120*1024)){printf("pthread_attr_setstacksize() failure:%s\n", strerror(errno));return -1;}/*将线程的属性设置为分离状态,分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。*/if(pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED)){printf("pthread_attr_setdetachstate() failure:%s\n", strerror(errno));return -1;}/*创建的work1是分离状态,结束后马上释放系统资源*/pthread_create(&tid, &thread_attr, thread_worker1, &shared_var);printf("Thread worker1 tid[%ld] created ok\n", tid);/*创建的work2是非分离状态,所有的线程等待创建的线程结束后,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源*/pthread_create(&tid, NULL, thread_worker2, &shared_var);printf("Thread worker2 tid[%ld] created ok\n", tid);/*摧毁释放线程属性*/pthread_attr_destroy(&thread_attr);/*work2是非分离状态,需要调用pthread_join() 等待第二个子线程(work2)退出,当然主线程也就阻塞在这里不会往下继续执行了。*/pthread_join(tid, NULL);while(1){printf("Main/Control thread shared_var:%d\n", shared_var);sleep(10);}return 0;
}void *thread_worker1(void *args)
{int *ptr = (int *)args;if(!args){printf("%s() get invalid arguments\n", __FUNCTION__);pthread_exit(NULL);}printf("Thread worker1 [%ld] start running...\n", pthread_self());while(1){printf("===worker[1]===:%s before shared_var: %d\n", __FUNCTION__, *ptr);*ptr += 1;sleep(2);printf("===worker[1]===:%s after sleep shared_var: %d\n", __FUNCTION__, *ptr); }printf("Thread worker1 exit...\n");return NULL;
}void *thread_worker2(void *args)
{int *ptr = (int *)args;if(!args){printf("%s() get invalid arguments\n", __FUNCTION__);pthread_exit(NULL);}printf("Thread worker2 [%ld] start running...\n", pthread_self());while(1){printf("===worker[2]===:%s before shared_var: %d\n", __FUNCTION__, *ptr);*ptr += 1;sleep(2);printf("===worker[2]===:%s after sleep shared_var: %d\n", __FUNCTION__, *ptr); }printf("Thread worker2 exit...\n");return NULL;
}
在创建两个线程时,我们都通过第四个参数将主线程栈中的 shared_var 变量地址传给了子线程,因为所有线程都是在同一进程空间中运行,而只是子线程有自己独立的栈空间,所以这时所有子线程都可以访问主线程空间的shared_var变量。
运行结果:
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao$ gcc thread.c -o thread -lpthread
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao$ ./thread
Thread worker1 tid[140508598748736] created ok
Thread worker2 tid[140508598621760] created ok
Thread worker2 [140508598621760] start running...
===worker[2]===:thread_worker2 before shared_var: 1000
Thread worker1 [140508598748736] start running...
===worker[1]===:thread_worker1 before shared_var: 1001
===worker[1]===:thread_worker1 after sleep shared_var: 1002
===worker[1]===:thread_worker1 before shared_var: 1002
===worker[2]===:thread_worker2 after sleep shared_var: 1002
===worker[2]===:thread_worker2 before shared_var: 1003
===worker[2]===:thread_worker2 after sleep shared_var: 1004
===worker[1]===:thread_worker1 after sleep shared_var: 1004
===worker[1]===:thread_worker1 before shared_var: 1004
===worker[2]===:thread_worker2 before shared_var: 1004
主线程创建子线程后究竟是子线程还是主线程先执行,或究竟哪个子线程先运行系统并没有规定,这个依赖操作系统的进程调度策略。当然因为主线程调用了pthread_join会导致主线程阻塞,所以主线程不会往下继续执行while(1)循环。
从上面的运行结果我们可以看到,thread_worker2 在创建后首先开始运行,在开始自加之前值为初始值1000,然后让该值自加后休眠2秒后再打印该值发现不是1001而是1002了。这是由于shared_var 这个变量会被两个子线程同时访问修改导致。如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源,那么对于该资源访问修改相关的代码就叫做临界区。
那么我想让上面的代码中的变量让work1和work2分别改动,不能在一方sleep的时候去改动,带怎么办呢?那我们就上锁:work1执行的时候上锁,work2来的时候看见work1上锁了,就在外面等待(阻塞),或者等会儿再来看看(非阻塞)。
我们来尝试一下用锁的机制来解决共享资源的问题,来修改一下代码:
#include
#include
#include
#include
#include void *thread_worker1(void *args);
void *thread_worker2(void *args);typedef struct worker_ctx_s
{int shared_var;pthread_mutex_t lock;
}work_ctx_t;//因为要要传两个参数,所以只能用结构体封装起来int main(int argc, char *argv[])
{work_ctx_t worker_ctx;pthread_t tid;//线程IDpthread_attr_t thread_attr;worker_ctx.shared_var = 1000;pthread_mutex_init(&worker_ctx.lock, NULL);//互斥锁初始化,一般设置为nullif(pthread_attr_init(&thread_attr)){printf("pthread_attr_init() failure: %s\n",strerror(errno));return -1;}if(pthread_attr_setstacksize(&thread_attr, 120*1024)){printf("pthread_attr_setstacksize() failure:%s\n", strerror(errno));return -1;}if(pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED)){printf("pthread_attr_setdetachstate() failure:%s\n", strerror(errno));return -1;}pthread_create(&tid, &thread_attr, thread_worker1, &worker_ctx);printf("Thread worker1 tid[%ld] created ok\n", tid);pthread_create(&tid, NULL, thread_worker2, &worker_ctx);printf("Thread worker2 tid[%ld] created ok\n", tid);pthread_attr_destroy(&thread_attr);pthread_join(tid, NULL);while(1){printf("Main/Control thread shared_var:%d\n", worker_ctx.shared_var);sleep(10);}pthread_mutex_destroy(&worker_ctx.lock);
}void *thread_worker1(void *args)
{work_ctx_t *ctx = (work_ctx_t *)args;if(!args){printf("%s() get invalid arguments\n", __FUNCTION__);pthread_exit(NULL);}printf("Thread worker1 [%ld] start running...\n", pthread_self());while(1){/*申请锁,非阻塞锁,如果被占用返回非0,没有占用返回0*/if(0 != pthread_mutex_trylock(&ctx->lock)){continue;};//非阻塞锁,没上锁就就上锁,上锁了就返回,该干嘛干嘛printf("===worker[1]===:%s before shared_var: %d\n", __FUNCTION__, ctx->shared_var);ctx->shared_var += 1;sleep(2);printf("===worker[1]===:%s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);pthread_mutex_unlock(&ctx->lock);//解锁sleep(1);}printf("Thread worker1 exit...\n");return NULL;
}
void *thread_worker2(void *args)
{work_ctx_t *ctx = (work_ctx_t *)args;if(!args){printf("%s() get invalid arguments\n", __FUNCTION__);pthread_exit(NULL);}printf("Thread worker2 [%ld] start running...\n", pthread_self());while(1){pthread_mutex_lock(&ctx->lock);//阻塞锁,第二个线程上锁,一直等到第二个线程解锁然后执行printf("===worker[2]===:%s before shared_var: %d\n", __FUNCTION__, ctx->shared_var);ctx->shared_var += 1;sleep(2);printf("===worker[2]===:%s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);pthread_mutex_unlock(&ctx->lock);//用完之后解锁sleep(1);}printf("Thread worker2 exit...\n");return NULL;
}
pthread_mutex_lock() 来申请锁,这里是阻塞锁,如果锁被别的线程持有则该函数不会返回;
pthread_mutex_unlock()来释放锁,这样其他线程才能再次访问;
pthread_mutex_trylock()来申请锁,这里使用的是非阻塞锁;如果锁现在被别的线程占用则返回非0值,如果没有被占用则返回0;
运行结果:
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao$ gcc thread_lock.c -o thread_lock
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao$ ./thread_lock
Thread worker1 tid[140619272656448] created ok
Thread worker2 tid[140619272529472] created ok
Thread worker1 [140619272656448] start running...
===worker[1]===:thread_worker1 before shared_var: 1000
Thread worker2 [140619272529472] start running...
===worker[1]===:thread_worker1 after sleep shared_var: 1001
===worker[2]===:thread_worker2 before shared_var: 1001
===worker[2]===:thread_worker2 after sleep shared_var: 1002
===worker[1]===:thread_worker1 before shared_var: 1002
===worker[1]===:thread_worker1 after sleep shared_var: 1003
===worker[2]===:thread_worker2 before shared_var: 1003
===worker[2]===:thread_worker2 after sleep shared_var: 1004
===worker[1]===:thread_worker1 before shared_var: 1004
如果多个线程要调用多个对象,则在上锁的时候可能会出现“死锁”。举个例子: A、B两个线程会同时使用到两个共享变量m和n,同时每个变量都有自己相应的锁M和N。 这时A线程首先拿到M锁访问m,接下来他需要拿N锁来访问变量n; 而如果此时B线程拿着N锁等待着M锁的话,就造成了线程“死锁”。

死锁产生的4个必要条件:
当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。
破坏“占有且等待”条件:
破坏“不可抢占”条件:
破坏“循环等待”条件: