浅说 fork

文章转载请注明出处,加上原文链接,谢谢! https://blog.csdn.net/weixin_46959681/article/details/112547656



程序与进程的区别

程序通常以二进制数据形式存在,被放置在存储媒介中(如硬盘、光盘、磁带、软盘等等),以物理文件的形式存在

一个程序被加载到内存当中运行,执行者的权限与属性、程序的代码与所需数据等都会被加载到内存中,操作系统赋予这个内存中的单元一个标识符 (PID) , 进程就是一个正在运行中的程序


进程标识符(PID)

执行任意一个程序或者命令,就可以产生一个进程。 为了让 Linux 系统便于管理被触发的进程,进程会给予执行者权限与属性等参数,以及进程所需要的脚本或数据等,最后再给予一个进程标识符 PID 。操作系统会根据此 PID 来判断该进程是否具有执行权限。

进程标识符 PID

《鸟哥的Linux私房菜(第四版)》P516

函数原型: pid_t fork(void);

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv[])
{
    
    
        printf("Pid = %d\n", getpid());
        return 0;
}

运行结果:
test.c


函数 fork 的功能

演示代码: fork.c

//fork.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    
    
        pid_t pid;
        pid = fork();
        if(pid > 0){
    
    
                while(1)
                {
    
    
                        printf("This is father pro and id is %d.\n", getpid());
                        sleep(1);
                }
        }
        else if(pid == 0){
    
    
                while(1)
                {
    
    
                        printf("This is child pro and id is %d.\n", getpid());                       
                        sleep(1);
                }
        }
        return 0;
}

运行结果:
fork.c

结果分析:

从运行结果分析,调用函数 fork 后在循环的作用下一共打印了两次,并且两个进程相互交替出现。由此引出函数 fork功能

  • 从一个原有进程中创建一个新进程,称其为子进程,原进程被称为父进程。两个进程共用寄存器 ( register ) 和程序计数器 ( PC )。

  • 子进程相当于父进程的副本,子进程获得父进程的数据空间、栈副本、堆副本。但是两者并不共享这些存储空间。

从代码的分支来看,当调用 fork 成功则返回两次,进程标识符 PID 一共有两个 返回值 :

  • 返回值为 0 , 代表当前进程为 子进程 。
  • 返回值为 非负数 ,代表当前进程为 父进程 。(父进程的返回值同时也是新的子进程的进程标识符 PID 。)
  • 返回值为 -1 ,调用失败。

|调用 fork 为什么会产生两次返回值?

从演示代码以及运行结果可以看出,调用 fork 有两份各自独立的地址空间在运行。 原因在于子进程复制了父进程的堆、栈,所以两个进程都停留在 fork 函数中等待返回。 一次是在父进程中返回,一次是在子进程中返回,所以产生了两次不一样的返回值。

下面的例子,可以验证两个各自独立的地址空间运行。

#include <stdio.h>
#include <stdlib.h>
#include <erron.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    
    
        int a = 10;
        pid_t pid;
        printf("The father pid is %d.\n",getpid());

        pid = fork();
        if(pid < 0){
    
    
				perror("fork");
				exit(-1);
		}
        if(pid > 0){
    
    

                printf("This id father pro and its id equal %d.\n",getpid());
                printf("In father pro, a = %d\n",a);
        }
        else if(pid == 0){
    
    

                printf("This is child pro and its id equal %d.\n",getpid());
                a = a + 10;
                printf("In child pro, a = %d\n",a);
        }
        return 0;

}

运行结果:

fork.c

很明显,在各自进程的范围内修改了变量 a 的值,对各自的进程不产生影响。

|调用fork创建一个子进程的一般目的?

  1. 一个父进程通过地址自己,使父、子进程同时执行不同的代码段。在请求频繁的网络服务中,父进程等待客户端的服务请求。接收到请求时,父进程调用 fork 创建子进程,使子进程处理此请求,父进程则继续等待下一个服务请求到达。
  2. 一个父进程要执行一个不同的程序,这对 shell 是常见的情况。在这种情况下子进程从 fork 返回后立即调用 exec 。(在后面的博客中会提及。)

C程序的存储空间分配

在这里插入图片描述

正文段/代码段(code segment/text segment)。 通常是指用来存放程序执行代码的一块内存区域,这是CPU执行的机器指令部分。正文段是可共享的,所以频繁执行的程序(如文本编辑器vim,C编译器的和shell等)在存储器中也只能有一个副本。这部分区域的大小在程序运行前就已经确定,正文段常常是只读的,以防止程序由于意外而修改其自身的指令。

初始化数据段/数据段(data segment)。 其包含了程序中需明确地赋值的变量,属于静态内存分配。

未初始化数据段/BBS段。 属于静态内存分配,指用来存放程序中未初始化的全局变量的一块内存区域。BBS —— 这一名称来源于一个早起的汇编运算符,意思是“block started by symbol”(由符号开始的块),在程序开始执行之前,内核将此段的数据初始化为0或空指针NULL。出现在任何函数外的C声明

long sum[1000];

使此变量存放在非初始化数据段中。

栈(stack)。 自动变量以及每次函数调用是所需要的保存的信息都存放在此段中。每次调用函数时,其返回地址以及调用者的环境信息(例如某些机器寄存器的值)都存放在栈中。然后,最近被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,可以递归调用C函数。递归函数每次调用自身时,就使用一个新的栈帧,因此一个函数调用实例中的变量集不会影响另一个函数点用实例中的变量。

堆(heap)。 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。由于历史上的惯例,堆位于非初始化数据段和栈之间。


fork 编程实战总结

一个现有进程可以调用函数 fork 创建一个新进程,其被称为子进程 。fork 被调用一次,但返回两次。区别在于子进程的返回值是0,父进程的返回值是新的子进程的 ID 。将子进程的 ID 返回给父进程的理由是:因为一个进程的子进程可以可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的 ID 。fork 使子进程得到返回值为0的理由是:一个进程只会有一个父进程,所有的子进程总是可以调用 getpid() 以获得其父进程的 进程ID(进程ID 0总是内核交换进程使用,所有一个子进程的 进程ID 不可能为0。 )

子进程和父进程继续执行 fork 调用之后的指令。子进程是父进程的副本,如子进程获得父进程的数据空间、队和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程,共享正文段。

由于在 fork 之后经常跟随 exec ,所有现在很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用了写时复制(Copy-On-Write, COW)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读。如果父、子进程中任何一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。


参考资料


文章更新记录

  • 文章完成上半段。 「2021.1.14 22:39」
  • 文章完成下半段。 「2021.1.16 20:56」
  • 增加“C程序的存储空间分配”一节。 「2021.1.17 10:23」
  • 文章版式调整以及内容修改了三分之一。 「2021.1.17 15:11」

P.S. 最近好心烦意乱呀!学习进度拖了将近一个星期了……

猜你喜欢

转载自blog.csdn.net/weixin_46959681/article/details/112547656