进程之fork 进程之fork

父子进程的不同点

子进程和父进程相同点多于不同点,一般来说我们记住不同点会更加省事

这里只列出了部分不同点

  • fork返回值不同
  • 进程ID不同
  • 父进程ID不同
  • 子进程不继承父进程的文件锁
  • 子进程未处理的闹钟被清除
  • 子进程未处理信号集设置为空集

父子进程的拷贝现象

子进程与父进程环境几乎是一样的,它几乎拷贝了父进程的所有内容(为了减少消耗使用了写时复制技术),在这里,我得说明一下"拷贝"含义,"拷贝"是指父进程与子进程各自有一份资源副本,他们对各自资源副本的修改不会对对方产生任何影响.

子进程从父进程继承的属性

这里我说的是继承,但实际上等同于拷贝,他们对这些资源的修改不会对对方产生任何影响

这里只列出了一部分

  • 实际UID,实际GID,有效UID,有效GID
  • 进程组ID
  • 会话ID
  • 控制终端
  • 当前工作目录
  • umask屏蔽字
  • 已打开的文件描述符及其文件表项(如对描述符执行了dup函数那样)
  • 资源限制
  • 根目录
  • 标准输出缓冲区

直观感受拷贝现象

写代码时,我们可以通过下面的图片来直观地感受到拷贝所发生的位置

进程之fork
进程之fork

从内存布局中理解拷贝现象

上图中看到的是一个直观的现象,但没有深入到本质,接下来我们从内存布局的角度上进行更深入地了解

进程之fork
进程之fork

上图是Linux32位x86架构上运行的进程的标准内存布局

而子进程实际上拷贝的是父进程的

  • 栈(Stack)
  • 堆(Heap)
  • BSS段(BSS Segment)
  • 数据段(Data Segment)
  • 代码段(Text Segment)

没有被拷贝的部分,或者说共享的部分是

  • 内核虚拟空间(Kernel Space)
  • 文件映射区(Memory Mapping Segment)

下面我们来分析一个经典的问题来加深对进程拷贝的理解

int main(void)
{
	int i;
	for(i=0;i<2;i++)
	{
		fork();
		printf("X");
	}
	return 0;
}
//执行上述程序后,其输出结果是?

在分析上述程序时,我们需要注意两个发生拷贝的地方

  • i变量
  • 标准输出缓冲区 标准输出默认是行缓冲的,所以父进程的缓冲区同样会被拷贝一份到子进程当中,只有遇到 或者进程终止时才把缓冲区内容flush到控制终端

理解了上面两点之后,我们随之画出它的状态图,如下图

进程之fork
进程之fork

图中右上角是状态图的含义, 可以看到,该段代码创建了四个进程, 当每个进程都执行完最后一句printf后退出时,他们将清空标准输出缓冲区,控制终端上将会出现XXXXXXXX ,即八个X

父进程和子进程谁先执行?

fork之后,应假设两个进程同时运行,甚至能在汇编甚至机器代码层面上交替运行,其执行过程的分析方法与线程相同.这里我留到讲线程的专题中讲解.

进程同步

一般来说,进程是分离的个体,应尽量减少信息交互(因其交互过程复杂,更好地替代方法是使用线程),但如果我们确实要如此,进程之间也是可以共享信息的.

进程通信需要满足两个条件

  • 一有通讯方法
    • 管道
    • 命名管道
    • 消息队列
    • 信号量
    • 共享内存
    • 网络Socket
  • 二有同步方法

这里我不打算涉及进程通讯方法,而只讨论进程同步方法,下面给出了同步进程锁的简单实现例子

简易进程调度器

#include <stdio.h>
#include <getopt.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>
#define TASK_N 3   //fork()创建的工作进程个数

//工作进程工作是否完成
enum status{
    FINISHED,
    NOTYET
};

//描述工作进程的结构体
struct task{
    pid_t  pid;
    pthread_mutex_t m;
    enum status s;
};

int main(int argc,char* argv[]) {

    struct task* tasks[TASK_N];
    //每个子进程都保留一个与父进程互斥的锁,父进程轮流选中工作进程,被选中的子进程可以展开工作,没被选中的子进程将保持待命状态
    //这里需要注意两点:
    //1. 必须把锁映射到文件映射区,否则将会导致lock()将会失效
    //2. 锁的属性需要设置为进程共享的 PTHREAD_PROCESS_SHARED
    for(int i=0;i<TASK_N;i++)
    {
        tasks[i] = (struct task*)mmap(NULL,TASK_N*sizeof(struct task),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
    }

    pid_t pid;
    
    //初始化锁的属性以及工作进程状态
    pthread_mutexattr_t attr;
    for(int i=0;i<TASK_N;i++)
    {
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
        pthread_mutex_init(&tasks[i]->m,&attr);
        pthread_mutex_lock(&tasks[i]->m);
        tasks[i]->s = FINISHED;
    }
    pthread_mutexattr_destroy(&attr);
    
    for(int i=0;i<TASK_N;i++)
    {
        if((pid =fork()) < 0){
            perror("fork failed!");
            return -1;
        }else if(pid == 0){
            pid_t cpid = getpid();
            //记录子各子进程ID
        	tasks[i]->pid = cpid;
            printf("child[%d] fork successfully!
",cpid);
            while(1)
            {
                sleep(2);
				
                /*debug:print task list
                for(int k=0;k<TASK_N;k++)
                {
                    printf("tasks[%d].pid=%d
",k,tasks[k]->pid);
                }*/

                int j;
                //子进程找到自己的锁
                for(j=0;j<TASK_N;j++)
                {
                    if(tasks[j]->pid == cpid)break;
                }
                if(j>=TASK_N){
                    fprintf(stderr,"child cannot find himself!
");
                    return -2;
                }

                //printf("child[%d] waiting for lock...
",tasks[j]->pid);
                pthread_mutex_lock(&tasks[j]->m);
                printf("I'm child[%d]. I have lock!.I can do myjob...
",tasks[j]->pid);
                sleep(1);
                printf("child[%d]: job done ,give lock to manager!
",tasks[j]->pid);
                tasks[j]->s = FINISHED;
                pthread_mutex_unlock(&tasks[j]->m);
            }//child will not go out for loop
            
        }
		


    }

    sleep(1);
	
    //轮流选中子进程,被选中子进程开始工作
    while(1)
    {
        for(int i=0;i<TASK_N;i++)
        {
            printf("manager: child[%d] go!
",tasks[i]->pid);
            tasks[i]->s = NOTYET;
            pthread_mutex_unlock(&tasks[i]->m);
            while(tasks[i]->s != FINISHED);
            pthread_mutex_lock(&tasks[i]->m);
        }
    }

    return 0;
}
代码执行结果:

child[48490] fork successfully!
child[48492] fork successfully!
child[48491] fork successfully!
manager: child[48490] go!
I'm child[48490]. I have lock!.I can do myjob...
child[48490]: job done ,give lock to manager!
manager: child[48491] go!
I'm child[48491]. I have lock!.I can do myjob...
child[48491]: job done ,give lock to manager!
manager: child[48492] go!
I'm child[48492]. I have lock!.I can do myjob...
child[48492]: job done ,give lock to manager!
manager: child[48490] go!
I'm child[48490]. I have lock!.I can do myjob...
child[48490]: job done ,give lock to manager!
manager: child[48491] go!
I'm child[48491]. I have lock!.I can do myjob...
child[48491]: job done ,give lock to manager!
manager: child[48492] go!
I'm child[48492]. I have lock!.I can do myjob...
child[48492]: job done ,give lock to manager!
manager: child[48490] go!
I'm child[48490]. I have lock!.I can do myjob...
child[48490]: job done ,give lock to manager!
manager: child[48491] go!
I'm child[48491]. I have lock!.I can do myjob...

Process finished with exit code 15

已知存在问题:

  • 子进程为了找到自己的锁需要遍历所有的子进程,这里做个哈希会好一些
  • 子进程如果先退出则会出现僵尸进程

参考资料

Linux内存布局

Linux内核层面内存布局

UNIX环境高级编程(APUE)