目录
一、fork()是什么?
二、fork返回值问题
1、fork()的两个返回值是什么?
2、fork()为什么有两个返回值?
3、一个变量为什么会保存两个不同的值?
三、写时拷贝
1、写时拷贝是什么
2、为什么要写时拷贝
3、写时拷贝的示意图
四、fork()创建子进程时系统做了什么?
五、fork()常规用法
六、fork()函数相关补充
七、fork失败的原因?
1、系统中有太多进程
2、实际用户的进程数超过了限制
fork()是操作系统提供的一个函数,它可以从代码层面(已经存在的进程)上创建一个新的进程。
新的进程为子进程,原进程为父进程。
给子进程返回0,给父进程返回子进程的pid
1 #include
2 #include
3
4 int main()
5 {
6 pid_t id=fork();
7 if(id<0)
8 {
9 printf("子进程创建失败\n");
10 return 1;
11 }
12 else if( id==0 )
13 {
14 //子进程
15 while(1)
16 {
17 printf("我是子进程:pid: %d,ppid: %d\n",getpid(),getppid());
18 sleep(1);
19 }
20 }
21 else
22 {
23 //父进程
24 while(1)
25 {
26 printf("我是父进程:pid: %d,ppid: %d\n",getpid(),getppid());
27 sleep(1);
28 }
29 }
30 }
因为fork()函数内部,return会被执行两次,return的本质就是对id进行写入
父子进程各自都有地址空间,页表,用的虚拟地址一样,物理内存是被映射到不同区域的,当任何一方尝试修改数据时,发生了写时拷贝,如果子进程修改数据时,同一个变量经过虚拟地址空间映射,页表写时拷贝,会重新给子进程开辟空间,所以父子进程各自其实在物理内存中,有属于自己的变量空间,只不过在用户层用同一个变量(虚拟地址)来标识。
写时拷贝时多进程创建时,保证父子进程独立性,尤其是数据层面上保证独立性的非常关键的策略,采用写时拷贝技术实现独立性,通过写时拷贝让父子进程数据不在相互干扰,也是一种延迟写入的过程,宏观上提高了整个内存的使用率。
因为有写时拷贝技术的存在,所以父子进程得以彻底分开,完成了进程独立性的技术保证,
写时拷贝是一种延时申请技术,可以提高整机内存的使用率
对于数据而言
创建进程的时候,直接拷贝分离,可能拷贝到子进程根本就不会用到的数据空间,即便用到了,也可能只是读取,会造成空间浪费,因此创建子进程时,不需要将不会被访问的,或者只会读取的数据拷贝一份。
什么样的数据值得拷贝?
将来被父或者子进程写入的数据,但是即使是OS,无法知道哪些空间会被写入,即便提前拷贝了,可能并不会立即使用
所以OS选择了写时拷贝技术,来进行将父子进程的数据进行分离
因此回答了OS为何要选择写时拷贝技术,对父子进程进行分离
1、用的时候,在分配空间,是高效使用内存的一种表现
2、OS无法在代码执行前预知哪些空间会被访问
3、写时拷贝保证了进程的独立性
fork()之后,子进程构建出来后数据结构以父进程为模板,把父进程相关字段拷贝起来,默认情况下指向的内容是完全一样的,对页表的读写权限为只读权限,如果子进程尝试修改物理内存中的内容,把内存中曾经被大家所共享的内存区域拷贝一份给子进程,修改页表当中的页表项,让子进程的页表指向新空间的地址,数据层面上两个进程实现了分离。任何一个进程尝试修改,都会发生写时拷贝,只读权限去掉了,两个进程已经分开,实现进程独立性。
fork()创建子进程,本质是系统里多了一个进程,该进程有对应的PCB,对应的地址空间,对应的页表,并将自己进程对应的代码和数据加载到内存,构建映射关系,将该进程PCB放入到运行队列里,等待OS或调度器调度,一旦CPU调度该进程,此时可以通过虚拟地址空间和页表找到该进程的相关代码,然后从上往下执行顺序语句,循环判断,函数跳转,进而在进程内部执行进程内的代码,完成某件事情。
因此fork()执行后,OS会分配新的内存块和内核数据结构给子进程(创建对象),将父进程部分数据结构内容拷贝给子进程(赋值和初始化,子进程的相关数据是以父进程为模板的,包括优先级信息,状态信息),添加子进程到系统进程列表当中(如果无法直接运行,就列入到等待队列,阻塞或者挂起)fork返回,开始调度器调度(fork函数最后return时,子进程已经创建)
1、一个父进程希望复制自己,使父子进程同时指向不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
2、一个进程要执行一个不同程序
1、创建子进程时,会给子进程分配对于的内核数据结构,这些都是子进程私有的,因为进程具有独立性!理论上,子进程也要有自己的代码和数据,可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据。我们知道代码时不可以被写入的,只能读取,数据可能会被修改,必须分离,因此创建子进程时,父子进程代码共享,数据以写时拷贝的方式各自持有一份
2、fork之后,父子进程代码共享是after之后还是所有代码共享?
1、我们的代码汇编后,会有很多行代码,每行代码加载到内存之后,都有对应的地址
2、因为进程随时可能被中断(还没执行完)下次回来还必须从之前的位置继续运行,就要求CPU必须记录下来当前进程执行的位置,所有CPU内有对应的寄存器数据,用来记录当前进程的执行位置!
3、寄存器在CPU内只有一份,寄存器内的数据可以有多份的,即进程的上下文数据,既然是数据创建的时候,要给子进程,虽然父子今进程各自调度,各自会修改EIP,但是已经不重要了,子进程已经认为EIP的起始值,就是fork之后的代码,子进程从after之后开始执行,并不代表之前的代码看不到。
OS系统创建一个进程要做什么?
要创建进程对应的内核数据结构task_struct,还要为该进程创建对应的地址空间mm_struct,还要为该进程创建页表,构建映射关系,并且还要将该进程对应的代码和数据加载到内存。
因此,进程创建本质要消耗内存资源,进程太多,导致系统内存资源不足,不让创建进程。
1 //fork失败原因
2 //系统内存不足
3 //系统给该用户创建的进程数是有上限的
4
5 //测试系统创建多少个进程数会失败
6 #include
7 #include
8 #include
9
10 int main()
11 {
12 int max_cnt=0;
13 pid_t id=fork();
14 while(1)
15 {
16
17 if(id<0)
18 {
19 //这个代码是父进程在执行 因为fork创建失败没有子进程执行
20 printf("fork error:man_cnt=%d\n",max_cnt);
21 break;
22 }
23 else if(id==0)
24 {
25 //child
26 while(1)
27 {
28 sleep(1);
29 }
30 }
31 else
32 {
33 //father
34 printf("max_cnt:%d\n",max_cnt);
35 }
36 max_cnt++;
37 }
38 }
39
40 //max_cnt是父子共享的
41 //但是写时拷贝对父进程是不影响的
42 //写时拷贝改的一直是父进程的
43
44 //子进程一直不退出 fork失败后父进程退出
45 //子进程变成孤儿进程