Linux进程管理 | fork 和 写时拷贝

目录

一.fork

二.写时拷贝

三.孤儿进程

四.僵死进程


一.fork

1.函数功能

复刻(英语:fork,又译作派生、分支)是UNIX或类UNIX中的分叉函数,fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程,fork以后的两个进程单独执行。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

fork产生的新进程叫做子进程,原来的进程(调用fork函数的那个进程)叫做父进程

2.函数返回值

子进程的返回值是0,父进程的返回值是新建子进程的PID。出错的话返回-1。因此可以用返回值来区分父子进程。

关于PID和进程的详解请参考:Linux进程管理 | 进程

注意,fork函数与普通函数不同,他会返回两次,一次返回给父进程,一次返回给子进程。由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,而这两次的返回值是不一样的。

3.父子进程的关系

fork之后,进程是父进程的不完全副本。子进程的数据区、bbs区、堆栈区(包括 I/O 流缓冲区),甚至参数和环境区都从父进程拷贝(这意味着父子进程间不共享这些存储空间),唯有代码区与父进程共享。 因为,代码区是可执行指令 、字面值常量 、具有常属性且被 初始化的全局、静态全局 和 静态局部变量。这也就意味着子进程有了自己独立的地址空间。

调用fork之前的代码只有父进程执行,而调用fork后的代码,父子进程都会执行。但是具体是谁先运行,依赖于系统的实现

4.fork的性质

  • 由 fork()创建的新进程被称为子进程。fork()函数调用一次,但返回两次。两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 PID。通过fork()的返回值来区别父子进程,如果出错返回-1。
  • 调用fork()前的代码只有父进程执行,fork()成功返回后的代码,父子进程都会执行
  • 将新建子进程 PID 返回给父进程的理由:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的 PID。
  • fork 使子进程得到返回值 0 的理由:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 PID(进程 PID 0 总是由内核交换进程使用,所以一个子进程的进程 PID 不可能为 0)。
  • 调用fork()出错的原因:系统中已经存在太多的进程;调用函数fork()的用户进程太多。

5.代码

new.c



#include
#include
#include
#include
#include

int main()
{
    printf("it is fork\n");
    pid_t n = fork();
    assert( n != -1);
    if(n==0)
    {
        printf("hello: %d\n",getpid());//getpid()得到当前进程的pid,getppid()获取父进程的pid
    }
    else
    {
        printf("world: %d\n",getpid());
    }
    printf("it will end\n");
    exit(0);
}


执行结果:

Linux进程管理 | fork 和 写时拷贝_第1张图片

6.关于fork的面试题

NO.1

#include 
#include 
#include 
#include 

int main()
{
	fork() || fork();
    printf("A");
    exit(0);
}

执行结果:

结果输出3个”A”,共创建3个进程。

fork()给子进程返回一个零值,而给父进程返回一个非零值;

在main这个主进程中,首先执行 fork() || fork(), 左边的fork()返回一个非零值,根据||的短路原则,前面的表达式为真时,后面的表达式不执行,故包含main的这个主进程创建了一个子进程,

由于子进程会复制父进程,而且子进程会根据其返回值继续执行,就是说,在子进程中, fork() ||fork()这条语句左边表达式的返回值是0, 所以||右边的表达式要执行,这时在子进程中又创建了一个进程,

即main进程->子进程->子进程,一共创建了3个进程。

类似与这样的面试题还有很多,我就不在这一一列举,有兴趣可以参考以下博客

https://blog.csdn.net/ZYZMZM_/article/details/87532320

https://blog.csdn.net/tzs_1041218129/article/details/55008173

https://blog.csdn.net/u012658346/article/details/51131519

二.写时拷贝

1.定义

传统的fork()系统调用直接把所有的资源复制给新创建的进程,这种实现过于简单并且效率低下。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间

简单的理解为:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间

2.文件共享

fork的一个特性是进程的所有打开文件描述符都被复制到子进程中,简单的理解为父子进程共享fork之前打开文件的读写漂移量。在 fork 之后处理文件描述符有以下两种常用的操作模式:

  • 父进程等待子进程完成:在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。
  • 父进程和子进程各自执行不同的程序段:在这种情况下,在 fork 之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

3.代码

现在当前路径下创建一个a.txt文件,内容随意。

接着创建一个No2.c

代码如下

#include
#incldue
#include
#include
#include
#include

int main()
{
    int fd = open("a.txt",O_RDWR);
    assert(fd!=-1);
    pid_t n = fork();
    assert(n!=-1);

    if(n==0)
    {
        char buff[128]={0};
        read(fd,buff,21);
        printf("child: %s\n",buff);
    }
    else
    {
        sleep(1);
        char buff[128]={0};
        read(fd,buff,39);
        printf("father:%s\n",buff);
    }
    exit(0);
}

执行结果

Linux进程管理 | fork 和 写时拷贝_第2张图片

三.孤儿进程

父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。如果父进程先于子进程终止,子进程即成为孤儿进程,同时被 init 进程收养,即成为 init 进程的子进程,因此 inti 进程又被称为孤儿院进程

四.僵死进程

子进程先于父进程退出,父进程没有通过wait方法获取子进程的退出码,从而子进程不得不自己保存退出码,又因为退出码保存在PCB进程块中,所以整个PCB也就无法释放。,导致子进程变成僵死进程

关于PCB的详解请参考:Linux进程管理 | 进程

如果父进程直到终止都未回收它的已成僵尸的子进程,init 进程会立即收养并回收这些处于僵尸状态的子进程,因此一个进程不可能既是孤儿进程同时又是僵尸进程。
一个进程成为僵尸进程需要引起注意,如果它的父进程长期运行而不终止,僵尸进程所占用的资源将长期得不到释放。

 

你可能感兴趣的:(Linux,fork,写时拷贝,僵死进程,孤儿进程,僵尸进程)