fork & exec时打开文件的变化

本文通过实验演示了Linux系统调用fork和exec的工作原理,特别是它们如何处理进程间的文件描述符。通过代码示例和命令行工具,展示了fork前后子进程继承父进程打开的文件,以及exec执行新程序时保持原有文件描述符不变的特点。

在分析linux系统调用forklinux系统调用execve时,已经知道:
1.fork时,子进程会复制父进程的打开文件描述符表
2.exec时,进程的打开文件描述符表保持不变

 

用以下代码观察fork,exec打开文件的变化情况:
父进程fork子进程,睡眠一定时间(方便命令行查看打开文件);
子进程fork孙进程,睡眠一定时间;
孙进程exec新程序,新程序也睡眠一定时间

/* openfiles.c */
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <fcntl.h>
  4 #include <errno.h>
  5 
  6 #define BUFSIZE 50
  7 #define error() \
  8 do {    \
  9         char buf[BUFSIZE];      \
 10         snprintf(buf, BUFSIZE, "[%s][%d][%d]\n", __FILE__, __LINE__, errno);    \
 11         perror(buf);    \
 12 } while(0);
 13 
 14 int main(int argc, char *argv[])
 15 {
 16         int pid, fd;
 17         int nsecs;
 18         char snsecs[20] = {0};
 19         int err;
 20         if (argc != 2) {
 21                 printf("command format: %s sleeptime\n", argv[0]);
 22                 return -1;
 23         }
 24 
 25         nsecs = atoi(argv[1]);
 26         if (nsecs < 0 || nsecs > 120) {
 27                 nsecs = 120;
 28         }
 29         sprintf(snsecs, "%d", nsecs);
 30 
 31         fd = open(argv[0], O_RDONLY);
 32         pid = fork();
 22                 return -1;
 23         }
 24 
 25         nsecs = atoi(argv[1]);
 26         if (nsecs < 0 || nsecs > 120) {
 27                 nsecs = 120;
 28         }
 29         sprintf(snsecs, "%d", nsecs);
 30 
 31         fd = open(argv[0], O_RDONLY);
 32         pid = fork();
 33         if (pid == 0) {
 34                 pid = fork();
 35                 if(pid == 0) {
 36                         err = execl("./exec_openfiles", "exec_openfiles", snsecs, (char *) 0);
 37                         if (err) {
 38                                 error();
 39                                 return -1;
 40                         }
 41                 } else if (pid < 0) {
 42                         error();
 43                         return -1;
 44                 }
 45         } else if (pid < 0) {
 46                 error();
 47                 return -1;
 48         }
 49 
 50         sleep(nsecs);
 51         close(fd);
 52         return 0;
 53 }

/* exec_openfiles.c */
  1 #include <stdio.h>
  2 #include <fcntl.h>
  3 
  4 int main(int argc, char* argv[])
  5 {       
  6         int fd; 
  7         int nsecs;
  8         
  9         if (argc != 2) {
 10                 printf("command format: %s sleeptime \n", argv[0]);
 11                 return -1;
 12         }
 13                 
 14         nsecs = atoi(argv[1]);
 15         if (nsecs <= 0 || nsecs > 120) {
 16                 nsecs = 120;
 17         }                       
 18                                 
 19         fd = open(argv[0], O_RDONLY);
 20         sleep(nsecs);
 21         close(fd);      
 22         return 0;       
 23 }               

/* Makefile */
  1 all:
  2         gcc openfiles.c -o openfiles
  3         gcc exec_openfiles.c -o exec_openfiles


用以下命令观察三个进程打开文件的变化情况:
查看当前终端并运行openfiles:

[redhat@localhost fork_exec_openfiles]$ tty
/dev/pts/6
[redhat@localhost fork_exec_openfiles]$ ./openfiles 90

 

通过终端查看刚才运行的进程:

[redhat@localhost fork_exec_openfiles]$ ps -t pts/6 -f 
UID        PID  PPID  C STIME TTY          TIME CMD
redhat    8165 13780  0 13:20 pts/6    00:00:00 ./openfiles 90
redhat    8166  8165  0 13:20 pts/6    00:00:00 ./openfiles 90
redhat    8167  8166  0 13:20 pts/6    00:00:00 exec_openfiles 90
redhat   13780  2851  0 Jun29 pts/6    00:00:00 bash


通过lsof命令观察以上进程打开的文件:

[redhat@localhost fork_exec_openfiles]$ lsof -p 8165
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
openfiles 8165 redhat  cwd    DIR  253,0     4096 1068237 /home/redhat/code/syscall/fork_exec_openfiles
openfiles 8165 redhat  rtd    DIR  253,0     4096       2 /
openfiles 8165 redhat  txt    REG  253,0     6245 1068231 /home/redhat/code/syscall/fork_exec_openfiles/openfiles
openfiles 8165 redhat  mem    REG  253,0   141536  134697 /lib/ld-2.12.so
openfiles 8165 redhat  mem    REG  253,0  1880776  134698 /lib/libc-2.12.so
openfiles 8165 redhat    0u   CHR  136,6      0t0       9 /dev/pts/6
openfiles 8165 redhat    1u   CHR  136,6      0t0       9 /dev/pts/6
openfiles 8165 redhat    2u   CHR  136,6      0t0       9 /dev/pts/6
openfiles 8165 redhat    3r   REG  253,0     6245 1068231 /home/redhat/code/syscall/fork_exec_openfiles/openfiles

[redhat@localhost fork_exec_openfiles]$ lsof -p 8166
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
openfiles 8166 redhat  cwd    DIR  253,0     4096 1068237 /home/redhat/code/syscall/fork_exec_openfiles
openfiles 8166 redhat  rtd    DIR  253,0     4096       2 /
openfiles 8166 redhat  txt    REG  253,0     6245 1068231 /home/redhat/code/syscall/fork_exec_openfiles/openfiles
openfiles 8166 redhat  mem    REG  253,0   141536  134697 /lib/ld-2.12.so
openfiles 8166 redhat  mem    REG  253,0  1880776  134698 /lib/libc-2.12.so
openfiles 8166 redhat    0u   CHR  136,6      0t0       9 /dev/pts/6
openfiles 8166 redhat    1u   CHR  136,6      0t0       9 /dev/pts/6
openfiles 8166 redhat    2u   CHR  136,6      0t0       9 /dev/pts/6
openfiles 8166 redhat    3r   REG  253,0     6245 1068231 /home/redhat/code/syscall/fork_exec_openfiles/openfiles
openfiles 8166 redhat    4r   REG  253,0     6245 1068231 /home/redhat/code/syscall/fork_exec_openfiles/openfiles

[redhat@localhost fork_exec_openfiles]$ lsof -p 8167
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
exec_open 8167 redhat  cwd    DIR  253,0     4096 1068237 /home/redhat/code/syscall/fork_exec_openfiles
exec_open 8167 redhat  rtd    DIR  253,0     4096       2 /
exec_open 8167 redhat  txt    REG  253,0     5184 1068238 /home/redhat/code/syscall/fork_exec_openfiles/exec_openfiles
exec_open 8167 redhat  mem    REG  253,0   141536  134697 /lib/ld-2.12.so
exec_open 8167 redhat  mem    REG  253,0  1880776  134698 /lib/libc-2.12.so
exec_open 8167 redhat    0u   CHR  136,6      0t0       9 /dev/pts/6
exec_open 8167 redhat    1u   CHR  136,6      0t0       9 /dev/pts/6
exec_open 8167 redhat    2u   CHR  136,6      0t0       9 /dev/pts/6
exec_open 8167 redhat    3r   REG  253,0     6245 1068231 /home/redhat/code/syscall/fork_exec_openfiles/openfiles
exec_open 8167 redhat    4r   REG  253,0     5184 1068238 /home/redhat/code/syscall/fork_exec_openfiles/exec_openfiles

注:
cwd:当前目录
rtd:根目录
txt:程序代码段(可看出exec后的代码段不同)
mem:映射到进程地址空间的动态库
0u,1u,2u:标准输入(对应当前的虚拟终端),标准输出,标准错误
3r,4r:打开的文件

 

内核导出到proc文件系统中的进程打开文件信息:

[redhat@localhost fork_exec_openfiles]$ ll /proc/8165/fd
总用量 0
lrwx------. 1 redhat redhat 64  6月 30 13:45 0 -> /dev/pts/6
lrwx------. 1 redhat redhat 64  6月 30 13:45 1 -> /dev/pts/6
lrwx------. 1 redhat redhat 64  6月 30 13:45 2 -> /dev/pts/6
lr-x------. 1 redhat redhat 64  6月 30 13:45 3 -> /home/redhat/code/syscall/fork_exec_openfiles/openfiles
[redhat@localhost fork_exec_openfiles]$ ll /proc/8166/fd
总用量 0
lrwx------. 1 redhat redhat 64  6月 30 13:45 0 -> /dev/pts/6
lrwx------. 1 redhat redhat 64  6月 30 13:45 1 -> /dev/pts/6
lrwx------. 1 redhat redhat 64  6月 30 13:45 2 -> /dev/pts/6
lr-x------. 1 redhat redhat 64  6月 30 13:45 3 -> /home/redhat/code/syscall/fork_exec_openfiles/openfiles
lr-x------. 1 redhat redhat 64  6月 30 13:45 4 -> /home/redhat/code/syscall/fork_exec_openfiles/openfiles
[redhat@localhost fork_exec_openfiles]$ ll /proc/8167/fd
总用量 0
lrwx------. 1 redhat redhat 64  6月 30 13:45 0 -> /dev/pts/6
lrwx------. 1 redhat redhat 64  6月 30 13:45 1 -> /dev/pts/6
lrwx------. 1 redhat redhat 64  6月 30 13:45 2 -> /dev/pts/6
lr-x------. 1 redhat redhat 64  6月 30 13:45 3 -> /home/redhat/code/syscall/fork_exec_openfiles/openfiles
lr-x------. 1 redhat redhat 64  6月 30 13:45 4 -> /home/redhat/code/syscall/fork_exec_openfiles/exec_openfiles

父进程:8165
fork的子进程:8166
exec的孙进程:8167

 

由以上数据可以看出:
1.对比父进程与子进程打开的文件,可知fork后子进程会保持父进程打开的文件不变;父子进程打开的文件在fork之后会相互独立,如上例的子进程新打开的文件4不会出现在父进程打开文件表中。
2.对比子进程与孙进程打开的文件,可知exec后孙进程会保持子进程打开的文件不变(注意子进程文件4是在exec之后打开的);子孙进程打开的文件在exec之后相互独立,如上例的子进程打开的文件4不会出现在孙进程中,孙进程打开的文件4不会出现在子进程中。

 

 

所以可以通过打开的文件描述符实现父子进程的通信。
如以下命令

[redhat@localhost fork_exec_openfiles]$ ps -o pid,ppid,comm | cat 
  PID  PPID COMMAND
 8771 13780 ps
 8772 13780 cat
13780  2851 bash

就用到了管道和打开文件描述符表来实现通信:

1.shell创建一个管道,并fork两个进程,8771和8772
2.将进程8771的标准输出dup2到管道的写端(如有必要dup2会自动关闭标准输出),然后exec装入ps镜像,ps开始执行,输出写到管道中
3.将进程8772的标准输入dup2到管道的读端(如有必要dup2会自动关闭标准输入),然后exec装入cat镜像,cat开始执行,从管道中读数据
由于fork与exec过程中,打开文件描述符表都不变,所以可以通过以上步骤来实现进程间管道通信。

&lt;think&gt;好的,用户的问题是关于forkexec在创建新进程的区别及用法。我需要结合用户提供的引用信息以及之前回答中关于主动迁移和负载均衡的内容来综合回答。首先,我需要明确forkexec各自的功能。 根据引用[2]和[3],fork用于创建新进程,而exec用于替换当前进程的映像。用户可能想知道两者的具体区别,比如进程创建的方式、资源管理、使用场景等。同,用户之前的问题涉及操作系统的进程调度,如主动迁移和负载均衡,可能需要考虑这些机制如何与forkexec交互。 用户可能混淆了forkexec的作用,认为exec也会创建新进程,但实际上exec只是替换当前进程的代码和数据。需要明确这一点,避免误解。此外,用户可能想知道在实际编程中如何结合使用这两个系统调用,比如在shell中执行命令的典型流程。 需要检查用户提供的引用内容,确保回答准确。引用[1]提到exec函数族替换当前进程,引用[2]和[3]详细说明了exec后的进程变化,而引用[4]进一步确认exec不创建新进程。因此,回答中需要强调fork创建新进程,而exec替换现有进程的内容。 另外,用户之前的回答中涉及进程迁移和调度,可能需要在区别中提到进程创建与调度机制的关系,但主要焦点还是forkexec的区别。需要保持回答结构清晰,分点对比,并给出示例说明它们的用法,比如在shell中的使用。 可能还需要考虑错误处理,比如fork失败的情况,或者exec执行后的代码不可达。同,提到vfork和clone等其他相关系统调用作为扩展,但以用户的问题为主。 最后,生成的相关问题需要引导用户深入理解,比如资源继承、性能影响或典型应用场景。确保引用标记正确,比如exec替换进程的部分引用用户提供的引用[2][3][4]。&lt;/think&gt;### forkexec在创建新进程的区别及用法详解 --- #### **一、核心机制对比** | **维度** | **fork()** | **exec()** | |-------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------| | **进程创建** | 创建**新进程**(子进程),子进程是父进程的**完整副本** | **不创建新进程**,仅替换当前进程的代码段、数据段、堆栈段等内存空间[^2][^3] | | **资源继承** | 继承父进程的**所有资源**(文件描述符、内存、信号处理等) | 继承原进程的**PID、PPID、文件描述符**(除非显式关闭)等元数据[^2][^4] | | **执行流程** | 子进程从`fork()`返回处开始执行,通过返回值区分父子进程 | 新程序从`main()`函数开始执行,原进程代码在`exec()`后**完全失效**[^3][^4] | | **典型应用场景** | 实现**多任务并行**(如Web服务器处理并发请求) | 实现**程序替换**(如shell执行外部命令) | --- #### **二、关键差异解析** 1. **内存空间变化** - **fork**: - 使用**写复制(Copy-On-Write)**技术: $$ \text{物理内存消耗} = \text{父进程内存} \times (1 - \frac{\text{共享页比例}}{100}) $$ - 子进程获得父进程虚拟内存的**独立映射表** - **exec**: - 完全**覆盖**当前进程的代码段、初始化数据段、堆栈段 - 新程序的`.text`段加载地址通常与旧程序无关 ```c // fork示例 pid_t pid = fork(); if (pid == 0) { // 子进程代码 } else { // 父进程代码 } // exec示例(execlp) execlp(&quot;ls&quot;, &quot;ls&quot;, &quot;-l&quot;, NULL); // 执行后后续代码不再运行 ``` 2. **资源管理差异** - **文件描述符继承**: | **操作** | **fork** | **exec** | |---------------------|----------|----------| | 保持打开状态 | ✔️ | ✔️ | | 使用`FD_CLOEXEC`标志| - | ❌ | - 通过`fcntl(fd, F_SETFD, FD_CLOEXEC)`可在exec自动关闭描述符[^2] 3. **性能开销** | **指标** | **fork()** | **exec()** | |----------------|--------------------------------|--------------------------------| | 内存拷贝开销 | 低(COW优化) | 高(需加载新程序到内存) | | 上下文切换频率 | 可能引发调度器介入 | 仅首次加载产生显著延迟 | | 典型耗 | 微秒级(依赖父进程内存大小) | 毫秒级(依赖磁盘I/O速度) | --- #### **三、协同工作模式** **经典组合:fork+exec**(shell命令执行标准流程) ```mermaid sequenceDiagram participant Shell participant Child_Process Shell-&gt;&gt;Child_Process: fork()创建子进程 Child_Process-&gt;&gt;Child_Process: 关闭不需要的文件描述符 Child_Process-&gt;&gt;Child_Process: 设置信号处理 Child_Process-&gt;&gt;Child_Process: exec()加载新程序 Child_Process--&gt;&gt;Shell: 执行完成/出错退出 ``` 1. **执行细节**: - 子进程通过`fork()`获得shell的环境上下文 - 调用`execvp()`会自动搜索`PATH`环境变量中的可执行文件[^1] - 若exec失败,子进程应调用`exit()`避免继续执行原代码 2. **错误处理示例**: ```c pid_t pid = fork(); if (pid == -1) { perror(&quot;fork failed&quot;); exit(EXIT_FAILURE); } else if (pid == 0) { execlp(&quot;gcc&quot;, &quot;gcc&quot;, &quot;test.c&quot;, &quot;-o&quot;, &quot;test&quot;, NULL); // 若exec失败才会执行以下代码 perror(&quot;exec failed&quot;); exit(EXIT_FAILURE); } else { wait(NULL); // 等待子进程结束 } ``` --- #### **四、高级特性对比** 1. **vfork()优化** - **设计目的**:避免fork的COW开销,在exec前**暂停父进程** - **内存行为**:子进程**共享父进程地址空间**直到调用exec或exit - **风险**:若子进程修改父进程数据将导致未定义行为 2. **clone()系统调用** - 允许更细粒度控制资源共享(如线程实现) - 通过参数选择共享的内容: ```c clone(child_func, stack_top, CLONE_VM | CLONE_FS, NULL); ``` --- #### **五、典型应用场景** | **场景** | **fork()使用** | **exec()使用** | |------------------------|---------------------------------------------|---------------------------------------------| | Shell命令执行 | 创建子进程隔离环境 | 加载`ls`/`gcc`等外部程序 | | 守护进程 | 通过两次fork避免僵尸进程 | 无(通常不替换自身代码) | | 多进程服务器 | 为每个连接创建独立处理进程 | 无(各进程运行相同服务代码) | | 热更新系统 | 创建新进程运行新版本 | 在新进程中加载更新后的二进制文件 | --- ### 关键区别总结表 | **特征** | **fork()** | **exec()** | |--------------------|-----------------------------|-----------------------------| | 进程ID | 生成新PID | 保持原PID | | 内存空间 | 初始为父进程副本 | 完全替换为新程序映像 | | 执行连续性 | 从fork返回处继续执行 | 从新程序的main函数开始执行 | | 典型返回值 | 子进程返回0,父进程返回子进程PID | 成功无返回,失败返回-1 | --- ### 相关问题 1. 为什么`fork()`后需要判断返回值?子进程如何避免执行父进程的代码? 2. 如何通过`exec()`系列函数传递环境变量? 3. `fork()`与`pthread_create()`在创建并发单元有何本质区别? 4. 在多线程程序中调用`fork()`会产生哪些风险?如何规避? [^1]: exec函数族通过替换进程内存实现程序加载 [^2]: fork创建子进程继承父进程资源 [^3]: exec保持原进程PID但替换内存内容 [^4]: 新程序从main函数开始执行且覆盖原进程数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值