当前位置: 首页 > news >正文

【Linux】fork函数详解

目录

1.  概述

2.  查询当前进程ID

3.  fork函数的运用

4.  延伸--验证存储空间

5.  vfork函数

6.  使用fork复制文件描述符


1.  概述

         fork创建一个新的进程,原进程称为父进程,新的进程称为子进程。fork创建进程后,函数在子进程返回0值,在父进程中返回子进程的PID。两个进程都有自己的数据段、BSS段、栈、堆等资源,父进程间不共享这些存储空间,而代码段为父进程和子进程共享。父进程和子进程各自从fork函数后开始执行代码,在创建子进程后,子进程复制了父进程打开的文件描述符,但是不复制文件锁。子进程未处理的闹钟定时被清除,子进程不继承父进程的未决信号集。

        函数表头文件:        

#include<unistd.h>

        函数原型:

/* Clone the calling process, creating an exact copy.Return -1 for errors, 0 to the new process,and the process ID of the new process to the old process.  */
extern __pid_t fork (void) __THROWNL;

        返回值,如果失败,则返回-1,如果成功,则返回进程PID。

        查看进程ID的函数:

pid_t getpid(void);
pid_t getppid(void);

2.  查询当前进程ID

        我们开始编写一个代码看看,首先创建一个fork_test.c文件用来存放我们后续工程,我们先来查看一下当前进程:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>int main(int argc, char const *argv[])
{printf("当前进程的ID为:%d\n",getpid());return 0;
}

        编写一个Makefile文件:

fork_test :fork_test.c-$(CC) -o $@ $^-./$@-rm ./$@

        可以看到此时的进程ID:

        我们如果想要通过终端命令查看当前进程,则需要再主函数添加一个 while() 循环,不要让进程结束过快,否则看不见:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>int main(int argc, char const *argv[])
{printf("当前进程的ID为:%d\n",getpid());while(1){}return 0;
}

3.  fork函数的运用

        我们上面知道,fork创建一个新的进程,原进程称为父进程,新的进程称为子进程。fork创建进程后,函数在子进程返回0值,在父进程中返回子进程的PID:

#include<stdio.h>    // 标准输入输出
#include<stdlib.h>   // 标准库函数
#include<unistd.h>   // Unix 标准函数(包含 fork(), getpid(), getppid())
#include <sys/types.h> // 系统类型定义(包含 pid_t)int main(int argc, char const *argv[])
{printf("当前进程的ID为:%d\n",getpid());pid_t pid = fork();if(pid == -1){printf("子进程创建失败!\n");return 1;}else if(pid == 0)//这里的代码都是新的子进程的{sleep(1); // 让父进程先执行printf("子进程创建创建成功%d,它的父进程为%d\n",getpid(),getppid());}else//这里的代码都是父进程的{sleep(2); // 等待子进程执行printf("我是父进程%d,我创建的子进程为%d\n",getpid(),pid);}return 0;
}

        这里需要注意,代码中需要加入sleep进行一个延时,如上述,子进程在休眠1秒期间,父进程有足够时间执行并退出,子进程醒来时父进程还在,所以能正确显示父进程ID。

        如果不加延时,或者父进程比子进程执行过快,就会出现,父进程执行太快,在子进程输出之前就退出了,子进程变成了"孤儿进程",被 init/systemd 进程收养:

        我们也可以通过更改延时看一下:

        我们可以创建一个for循环用来打印数据,去观察父进程和子进程的并发执行:

#include<stdio.h>    // 标准输入输出
#include<stdlib.h>   // 标准库函数
#include<unistd.h>   // Unix 标准函数(包含 fork(), getpid(), getppid())
#include <sys/types.h> // 系统类型定义(包含 pid_t)int main(int argc, char const *argv[])
{printf("当前进程的ID为:%d\n",getpid());pid_t pid = fork();if(pid == -1){printf("子进程创建失败!\n");return 1;}else if(pid == 0)//这里的代码都是新的子进程的{int i,a=5;for(i = 0;i < 5;i++){printf("son: %d\n",i);sleep(1); // 让父进程先执行           }printf("子进程创建创建成功%d,它的父进程为%d\n",getpid(),getppid());}else//这里的代码都是父进程的{int i,a=10;for(i = 5;i<a;i++){printf("father: %d\n",i);sleep(2); // 等待子进程执行}printf("我是父进程%d,我创建的子进程为%d\n",getpid(),pid);}return 0;
}

        运行结果:

        我们可以将上述结果拆分如下分析:

时间(秒)  父进程(PID:9542)       子进程(PID:9543)
--------------------------------------------------
t=0       father: 5             son: 0
t=1       (休眠中)               son: 1
t=2       father: 6             son: 2  
t=3       (休眠中)               son: 3
t=4       father: 7             son: 4
t=5       (休眠中)               子进程创建成功...
t=6       father: 8             (子进程结束)
t=7       (休眠中)
t=8       father: 9
t=9       我是父进程...

        更加正确的做法应当调用waitpid函数进行等待子进程的结束,而不是一味的通过时间进行判断,如:

#include<stdio.h>    // 标准输入输出
#include<stdlib.h>   // 标准库函数
#include<unistd.h>   // Unix 标准函数(包含 fork(), getpid(), getppid())
#include <sys/types.h> // 系统类型定义(包含 pid_t)
#include <sys/wait.h>  // 添加 waitpid 所需的头文件int main(int argc, char const *argv[])
{printf("当前进程的ID为:%d\n",getpid());pid_t pid = fork();if(pid == -1){printf("子进程创建失败!\n");return 1;}else if(pid == 0)//这里的代码都是新的子进程的{int i,a=5;for(i = 0;i < 5;i++){printf("son: %d\n",i);sleep(1); // 让父进程先执行           }printf("子进程创建创建成功%d,它的父进程为%d\n",getpid(),getppid());}else // 父进程{int i, a = 10;for(i = 5; i < a; i++){printf("father: %d\n", i);sleep(2);}printf("我是父进程%d,我创建的子进程为%d\n", getpid(), pid);// 等待子进程结束int status;waitpid(pid, &status, 0);printf("子进程 %d 已结束\n", pid);}return 0;
}

        这样做的好处就是可以防止孤儿进程的产生,假如我们将子进程的时间延长,父进程的时间缩短,如果不通过waitpid等待子进程结束,那么自己成就会变成孤儿进程:

        而如果我们加上,父进程需要等待子进程结束而结束:

4.  延伸--验证存储空间

        对于waitpid函数文章后续会详细解释,这里只是为了了解fork的调用,我们上面通过打印 i 和 a 大致可以看出进程间是并发执行的,我们下面再来看一下另一个点,两个进程都有自己的数据段、BSS段、栈、堆等资源,父进程间不共享这些存储空间,而代码段为父进程和子进程共享,这一点我们要怎么理解呢?我们在对上面的代码进行一个简单的修改,声明一个全局变量 count,为了方便观察现象,将父进程的延时在延长一点,通过for循环在父子进程中各自累加看看会是什么结果:

#include<stdio.h>    // 标准输入输出
#include<stdlib.h>   // 标准库函数
#include<unistd.h>   // Unix 标准函数(包含 fork(), getpid(), getppid())
#include <sys/types.h> // 系统类型定义(包含 pid_t)
#include <sys/wait.h>  // 添加 waitpid 所需的头文件int main(int argc, char const *argv[])
{printf("当前进程的ID为:%d\n",getpid());int i,count = 1;pid_t pid = fork();if(pid == -1){printf("子进程创建失败!\n");return 1;}else if(pid == 0)//这里的代码都是新的子进程的{for(i = 0;i < 9;i++){count++;printf("son: %d\n",count);sleep(1); // 让父进程先执行           }printf("子进程创建创建成功%d,它的父进程为%d\n",getpid(),getppid());}else // 父进程{for(i = 0; i < 3; i++){count++;printf("father: %d\n", count);sleep(3);}printf("我是父进程%d,我创建的子进程为%d\n", getpid(), pid);// 等待子进程结束int status;waitpid(pid, &status, 0);printf("子进程 %d 已结束\n", pid);}return 0;
}

        可以看出,当 fork() 创建子进程后,此时父子进程各有独立的 count 变量副本,初始值都是 1,父进程和子进程各自累计自己的count的值,没有共享存储空间:

5.  vfork函数

         vfork相较于fork的主要区别是fork要复制父进程的数据段;而vfork则不需要完全复制父进程的数据段,子进程与父进程共享数据段。

        fork不对父进程的执行次序进行限制,但是vfork需要子进程先运行、父进程挂起。

        我们对上面代码更改一下,将fork改为vfork:

#include<stdio.h>    // 标准输入输出
#include<stdlib.h>   // 标准库函数
#include<unistd.h>   // Unix 标准函数
#include<sys/types.h> // 系统类型定义
#include<sys/wait.h>  // waitpid 头文件int main(int argc, char const *argv[])
{printf("当前进程的ID为:%d\n",getpid());int i, count = 0;// 使用 vfork() 替代 fork()pid_t pid = vfork();if(pid == -1){printf("子进程创建失败!\n");return 1;}else if(pid == 0) // 子进程{// 警告:vfork() 子进程与父进程共享内存空间!// 修改 count 会影响父进程的 count 值for(i = 0; i < 5; i++){count++;  // 这会修改父进程的 count 变量!printf("son: %d\n", count);sleep(1);           }printf("子进程创建成功%d,它的父进程为%d\n", getpid(), getppid());// vfork() 子进程必须使用 _exit(),不能使用 return_exit(0);}else // 父进程{// 在 vfork() 中,父进程会等待子进程结束后才执行到这里// 注意:此时 count 已经被子进程修改过了!for(i = 0; i < 5; i++){count++;  // 在子进程修改的基础上继续增加printf("father: %d\n", count);sleep(1);}printf("我是父进程%d,我创建的子进程为%d\n", getpid(), pid);// 在 vfork() 中,由于子进程已用 _exit() 退出,通常不需要 waitpid()// 但为了代码清晰,可以保留int status;waitpid(pid, &status, 0);printf("子进程 %d 已结束\n", pid);}return 0;
}

        此时运行发现子进程先进行累加,然后父进程才进行运行,并且父子进程的数据是共享的:

特性fork()vfork()
内存复制写时复制(Copy-on-Write)不复制,共享父进程内存空间
执行顺序父子进程并发执行子进程先执行,父进程阻塞等待
性能相对较慢(需要设置页表)很快(几乎不消耗资源)
安全性安全,进程隔离危险,可能破坏父进程状态
使用场景通用进程创建后接 exec() 族函数
现代系统推荐使用已过时,不推荐使用

6.  使用fork复制文件描述符

        我们重新创建一个.c文件,先将我们需要使用的头文件全部引入,然后调用open函数创建一个.txt文件,open返回的文件描述符为 fd:

#include<stdio.h>     // 标准输入输出
#include<stdlib.h>    // 标准库函数
#include<unistd.h>    // Unix 标准函数(包含 fork(), getpid(), getppid())
#include<sys/types.h> // 系统类型定义(包含 pid_t)
#include<fcntl.h>     // 文件控制选项
#include <sys/stat.h> // 文件状态信息int main(int argc, char const *argv[])
{int fd = open("io.txt",O_CREAT | O_WRONLY | O_APPEND ,0644);if(fd == -1){printf("打开文件失败\n");perror("open");exit(EXIT_FAILURE);}return 0;
}

对于open函数的调用,不熟悉可以了解:

【Linux应用开发·入门指南】详解文件IO以及文件描述符的使用-CSDN博客

        然后在Makefile中编写代码:

fork_fd_test :fork_fd_test.c-$(CC) -o $@ $^-./$@-rm ./$@

        运行后发现创建一个 io.txt 文件:

        调用fork函数创建子进程:

    pid_t pid = fork();if(pid == -1){printf("子进程创建失败!\n");perror("fork");exit(EXIT_FAILURE);}else if(pid == 0){printf("子进程开始写入数据······\n");/*子进程需要写入的数据*/}else{printf("父进程开始写入数据······\n");       /*父进程需要写入的数据*/}

        我们可以通过strcpy函数进行数据复制,我们可以将想要写入的数据写入到缓冲区去,然后通过writer进行写入到io.txt文件当中:

#include<stdio.h>     // 标准输入输出
#include<stdlib.h>    // 标准库函数
#include<unistd.h>    // Unix 标准函数
#include<sys/types.h> // 系统类型定义
#include<fcntl.h>     // 文件控制选项
#include <sys/stat.h> // 文件状态信息
#include <string.h>   // 字符串操作
#include <sys/wait.h> // 进程等待int main(int argc, char const *argv[])
{int fd = open("io.txt", O_CREAT | O_WRONLY | O_APPEND, 0644);if(fd == -1){printf("打开文件失败\n");perror("open");exit(EXIT_FAILURE);}char buffer[1024];pid_t pid = fork();if(pid == -1){printf("子进程创建失败!\n");perror("fork");close(fd);exit(EXIT_FAILURE);}else if(pid == 0){// 子进程printf("子进程开始写入数据······\n");strcpy(buffer, "这是子进程写入的数据!\n");ssize_t bytes_write = write(fd, buffer, strlen(buffer));if (bytes_write == -1){perror("子进程write失败");} else {printf("子进程写入数据成功,写入 %zd 字节\n", bytes_write);}close(fd);printf("子进程写入完毕,并释放文件描述符\n");exit(EXIT_SUCCESS);  // 子进程明确退出}else{// 父进程printf("父进程开始写入数据······\n");       strcpy(buffer, "这是父进程写入的数据!\n");     ssize_t bytes_write = write(fd, buffer, strlen(buffer));if (bytes_write == -1){perror("父进程write失败");} else {printf("父进程写入数据成功,写入 %zd 字节\n", bytes_write);}// 等待子进程结束int status;waitpid(pid, &status, 0);printf("子进程已结束,状态: %d\n", status);close(fd);printf("父进程写入完毕,并释放文件描述符\n");}return 0;
}

        我们运行看一下:

        根据上述结果我们可以看出,子进程复制了父进程的文件描述符fd,二者指向的应是同一个底层文件描述(struct file结构体)。我们思考一个问题,子进程通过close()释放文件描述符之后,父进程对于相同的文件描述符执行write()操作仍然成功了。这是为什么?

        struct file结构体中有一个属性为引用计数,记录的是与当前struct file绑定的文件描述符数量。close()系统调用的作用是将当前进程中的文件描述符和对应的struct file结构体解绑,使得引用计数减一。如果close()执行之后,引用计数变为0,则会释放struct file相关的所有资源。

        我们通过图示来解释一下,最开始的时候,父进程open创建一个文件,其地址指向如下,此时引用计数为1:

        当我们使用fork后会创建一个子进程,其地址也是执行图示:

        此时的文件描述,引用计数就会变为2:

        因此虽然我们子进程调用close使引用计数减1,并不会马上关闭文件,灯父进程也调用close后才会正式清零关闭:

嵌入式Linux_时光の尘的博客-CSDN博客

http://www.dtcms.com/a/516357.html

相关文章:

  • 泰安市做网站的公司wordpress git项目
  • ON1 Photo RAW MAX(照片后期处理软件)
  • 天河网站建设哪家强哈尔滨市建设网站
  • 梦幻联动!卡尔曼滤波结合LSTM,精度提高19%!
  • 网站开发的硬件环境展览设计网站有哪些
  • 网站维护升级访问中网站基站的建设方案
  • 【JDK、JRE、JVM】
  • 临沂网站维护公司做网站怎么收费多少
  • Qoder 上线提示词增强功能,将开发者从“提示词”的负担中解放出来
  • 中国山东网站建设网站编辑人才队伍建设
  • FreeRTOS队列消息查询
  • 医院数字IP广播系统:基于内部局域网的分布式数字化医院IP广播
  • 中山骏域网站建设专家西部网站邮箱登录
  • FFmpeg --14-视频解码:h264解码为yuv
  • PixelShuffle原理
  • 昆明做网站价格网站屏蔽省份
  • 创建网站需要学什么知识2017民非单位年检那个网站做
  • LABVIEW依赖关系显示文件删除、移动或重命名,每次打开都要指定很多路径【解决方案】
  • 东莞网站建设seo浙江住房和城乡建设厅网站首页
  • MLOps 的CI/CD VS DevOps 的CI/CD
  • spark组件-spark sql-读取数据
  • 网站开发大致需要哪些步骤可视化开发工具推荐
  • zabbix实现配置监控Windows设备、SNMP协议设备的全流程实操教程
  • 天津做网站找哪家公司好建设网站公司哪里好相关的热搜问题解决方案
  • 友情链接价格seo官网制作规划
  • 桦甸市城乡建设局网站技术外包网站
  • 英文网站设计网络广告策划方案怎么做
  • go前后端项目的启动 、打包和部署
  • redis三主三从集群升级6.2.20, 保留数据
  • 导入部署天机AI助手智能体的全流程(详细图解,包含导入虚拟机后无法ping通百度的解决办法)