type
Post
status
Published
slug
2023/07/04/Call-the-Linux-system-call-fork()-to-create-a-process-experiment
summary
tags
开发
Linux
思考
category
Linux
icon
password
new update day
Property
Oct 22, 2023 01:31 PM
created days
Last edited time
Oct 22, 2023 01:31 PM
系统调用简介
操作系统为用户态运行的进程与硬件设备(如
CPU、磁盘、打印机等等)进行交互提供了一组接口。在应用程序和硬件之间设置这样一个接口层具有很多优点,首先,这使得编程更加容易,把用户从学习硬件设备的低级编程特性中解放出来。其次,极大地提高了系统的安全性,内核在要满足某个请求之前就可以在接口级检查这种请求的正确性。最后,更重要的是,这些接口使得程序更具有可移植性,因为只要不同操作系统所提供的一组接口相同,那么在这些操作系统之上就可以正确地编译和执行相同的程序。这组接口就是所谓的“系统调用”。
fork 系统调用
在 Linux 系统中,如何创建一个进程,就是通过调用系统调用
fork()
,fork()
创建一个新进程,fork()
本身就是分叉的意思,也就是当执行 fork()
以后,一个新的进程就诞生了,也就是父子进程都存在了,执行流就一分为二,fork()
给父进程返回子进程的 pid,给子进程返回 0, 如图所示:- fork 系统调用头文件:
<unistd.h>
;
- fork 系统调用的原型:
pid_t fork()
;
- fork 系统调用的返回值:pid_t 是进程描述符类型,本质就是一个 int。如果
fork()
函数执行失败,返回一个负数(<0);如果fork()
调用执行成功,返回两个值:0 和所创建子进程的 ID。
- fork 系统调用的功能:以当前进程作为父进程创建出一个新的子进程,并且将父进程的所有资源拷贝给子进程,这样子进程作为父进程的一个副本存在。父子进程几乎是完全相同的,但子进程与父进程的 ID 不同。
编写实验代码
编译、链接、运行,并反汇编
gcc -S hello.c -o hello.s // 编译 gcc -c hello.s -o hello.o // 汇编 gcc hello.o -o hello // 链接 ./hello // 执行 objdump -d hello // 反汇编
也可以用一步编译并链接:
gcc hello.c -o hello
为什么要反汇编,是为了看到程序执行前的样子到底是什么,也可以反汇编成 Intel 的汇编格式。
objdump -d hello.o -Mintel
执行结果
Before fork ... I am father. The pid of parent is: 164603 The pid of parent's child is: 164604 After fork, program exitting... I am child. The pid of child is: 164604 The pid of child's parent is: 164603 Child exitting...
实验讨论
- 编写代码,编译、汇编、链接以及反汇编,说说每一步都做了什么?你真切的学到了什么?
- 编写代码,即进行代码文本的编写
- 预处理,即将处理预处理指令如包含#include,宏定义制定#define等。
- 编译,即将预处理后的代码文件,编译成特定的汇编代码。
- 汇编,汇编过程将上一步的汇编代码转换成机器码,这一步产生的文件叫做目标文件,是二进制格式。
- 链接过程使用链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。附加的目标文件包括静态连接库和动态连接库。
- 在第 12 行执行
fork()
时系统进入到什么态? - 内核态。
- 结合 PPT 中的
fork()
的执行流,分析getpid()
的执行流。 - 主程序在执行的时候,执行到对应的 getpid() 系统调用,进入到内核态中,将对应的结果返回到用户态之后,完成所有的工作。
fork()
的执行对你有什么启发?- fork() 产生的子进程在父进程退出之后才执行的,通过执行结果我们可以发现,两者的代码都是一样的,确实是父进程的副本。但是为什么是父进程退出之后再执行的子进程,这样的话,父进程还可以对子进程进行回收处理吗?
答案
fork() 函数通过系统调用创建一个与原来进程几乎完全相同的进程。在 fork() 之后,父进程和子进程都处于就绪状态,开始由内核调度执行。谁先执行完全由调度器决定1。
父进程可以通过 wait() 或 waitpid() 函数等待子进程退出,并收集子进程的退出状态。如果子进程退出状态不被收集,则变成僵尸进程。子进程可以通过调用 _exit() 或 exit() 函数来退出。_exit() 函数直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构。exit() 函数在执行退出之前,会将文件缓冲区中的内容写回文件,即清理 I/O 缓冲2。
您提到的程序输出显示父进程先退出,这是因为父进程和子进程都是独立执行的,它们的执行顺序是不确定的。父进程可以在退出之前对子进程进行回收处理。
代码改进
原来代码的问题
- 父进程没有执行子进程的退出清理工作,可能会产生僵尸进程。
修改后的代码
#include <stdlib.h> #include <stdio.h> #include <unistd.h> int main(void) { pid_t pid; int status; printf("Before fork ...\n"); /* 调用 fork() 系统调用,创建一个子进程 */ switch (pid = fork()){ /* fork 失败,返回一个负值 */ case -1: printf("fork call failed\n"); fflush(stdout); exit(1); case 0: printf("I am child.\n"); printf("The pid of child is: %d\n", getpid()); printf("The pid of child's parent is: %d\n", getppid()); printf("Child exitting...\n"); exit(0); default: printf("I am father.\n"); printf("The pid of parent is: %d\n", getpid()); printf("The pid of parent's child is: %d\n", pid); wait(&status); printf("The child process exited with status: %d\n", status); } printf("After fork, program exitting... \n"); exit(0); }
执行结果
- 可以看到确实是两个进程是同时调度的。
- 父进程使用
wait()
函数来等待子进程退出。当子进程退出时,wait()
函数返回并将子进程的退出状态存储在status
变量中。父进程可以通过检查status
变量的值来确定子进程是如何退出的。此外,wait()
函数还会回收子进程的 PCB,从而避免产生僵尸进程。
Before fork ... I am father. The pid of parent is: 194255 The pid of parent's child is: 194256 I am child. The pid of child is: 194256 The pid of child's parent is: 194255 Child exitting... The child process exited with status: 0 After fork, program exitting...
参考资料
欢迎加入“喵星计算机技术研究院”,原创技术文章第一时间推送。
- 作者:tangcuyu
- 链接:https://expoli.tech/articles/2023/07/04/Call-the-Linux-system-call-fork()-to-create-a-process-experiment
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章