1、fork
现在P1用fork()函数为进程创建一个子进程P2,
a. fork
内核:
-
复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
-
为这四个部分分配物理块,P2的:正文段->PI的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2自己的数据段块(为其分配对应的块),堆->P2自己的堆块,栈->P2自己的栈块。如下图所示:同左到右大的方向箭头表示复制内容。
b. COW
写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
c.vfork
vfork():这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间
2.fork()函数
#include<unistd.h>
#include<sys/types.h>
pid_t fork( void);
(pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中)
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
#include<sys/types.h> //对于此程序而言此头文件用不到
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc, charchar ** argv ){
//由于会返回两次,下面的代码会被执行两遍
//如果成功创建子进程:
//1. 父进程返回子进程ID,因此(父进程)会走一遍“分支3”
//2. 子进程返回0,因此(子进程)会走一遍“分支2”
pid_t pid = fork();
if (pid < 0){ //分支1
fprintf(stderr, "error!");
}else if( 0 == pid ){//分支2
printf("This is the child process!");
_exit(0);
}else{//分支3
printf("This is the parent process! child process id = %d", pid);
}
//可能需要时候wait或waitpid函数等待子进程的结束并获取结束状态
exit(0);
}
由于文件的操作是通过文件描述符表、文件表、v-node表三个联系起来控制的,其中文件表、v-node表是所有的进程共享,而每个进程都存在一个独立的文件描述符表。
fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。
3. COW()函数
fork函数用于创建子进程,典型的调用一次,返回两次的函数,其中返回子进程的PID和0,其中调用进程返回了子进程的PID,而子进程则返回了0,这是一个比较有意思的函数,但是两个进程的执行顺序是不定的。fork()函数调用完成以后父进程的虚拟存储空间被拷贝给了子进程的虚拟存储空间,因此也就实现了共享文件等操作。但是虚拟的存储空间映射到物理存储空间的过程中采用了写时拷贝技术(具体的操作大小是按着页控制的),该技术主要是将多进程中同样的对象(数据)在物理存储其中只有一个物理存储空间,而当其中的某一个进程试图对该区域进行写操作时,内核就会在物理存储器中开辟一个新的物理页面,将需要写的区域内容复制到新的物理页面中,然后对新的物理页面进行写操作。这时就是实现了对不同进程的操作而不会产生影响其他的进程,同时也节省了很多的物理存储器。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
int main(){
char p = 'p';
int number = 11;
if(fork()==0) /*子进程*/
{
p = 'c'; /*子进程对数据的修改*/
printf("p = %c , number = %d \n ",p,number);
exit(0);
}
/*父进程*/
number = 14; /*父进程对数据修改*/
printf("p = %c , number = %d \n ",p,number);
exit(0);
}
$ gcc -g TestWriteCopyTech.c -o TestWriteCopyTech
$ ./TestWriteCopyTech
p = p , number = 14 -----父进程打印内容
$ p = c , number = 11 -----子进程打印内容
原因分析:
由于存在企图进行写操作的部分,因此会发生写时拷贝过程,子进程中对数据的修改,内核就会创建一个新的物理内存空间。然后再次将数据写入到新的物理内存空间中。可知,对新的区域的修改不会改变原有的区域,这样不同的空间就区分开来。但是没有修改的区域仍然是多个进程之间共享。
fork()函数的代码段基本是只读类型的,而且在运行阶段也只是复制,并不会对内容进行修改,因此父子进程是共享代码段,而数据段、Bss段、堆栈段等会在运行的过程中发生写过程,这样就导致了不同的段发生相应的写时拷贝过程,实现了不同进程的独立空间。
4、具体使用场景
1 fork使用场景
2. vfork分析
5、 fork代码分析
#include "apue.h"
int glob = 6;
int main(void)
{
int var;
pid_t pid;
var = 88;
int status;
printf("bdfore vfork\n");
if((pid = fork())<0)
{
err_sys("vfork error");
}else if(pid == 0)
{
glob++;
var++;
printf("son vfork\n");
printf("pid =%d,=%d\n",getpid(),pid);
if((pid=fork())<0)
{err_sys("fork_error");}
else if(pid>0){
//printf("second parent pid =%d,=%d\n",getpid(),pid);
exit(0);}
sleep(2);
printf("pid =%d,=%d\n",getpid(),pid);
printf("Second child, parent pid = %d\n", getppid());
exit(0);
}
else{
if(wait(&status)==pid)
{
printf("wait1");
}
// if(waitpid(pid,NULL,0)!=pid)//成功返回pid
// {
// err_sys("waitpid error");
// }
printf("--- fork,%d\n",pid);
}
printf("pid =%d,=%d glob = %d, var = %d\n",getpid(),pid,glob,var);
exit(0);
}
打印结果如下
bdfore vfork
son vfork
pid =25215,=0
wait1--- fork,25215
pid =25214,=25215 glob = 6, var = 88
pid =25216,=0
Second child, parent pid = 1
上述代码中,第二个children由于父进程exit了,所以其父进程为init,pid为1
运行顺序为,第一个子进程,第一个父进程,第二个子进程!
5、Linux进程的五个段
下面我们来简单归纳一下进程对应的内存空间中所包含的5种不同的数据区都是干什么的。
BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)若程序员不释放,则会有内存泄漏,系统会不稳定,Windows系统在该进程退出时由OS释放,Linux则只在整个系统关闭时OS才去释放(参考Linux内存管理)。
栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。