Linux系统应用编程---进程原语(fork、exec、wait&waitpid)

简介: Linux系统应用编程---进程原语(fork、exec、wait&waitpid)

fork

子进程复制父进程的0到3g空间和父进程内核中的PCB,但id号不同。

程序一讲解:

1. #include <stdio.h>
2. #include <sys/types.h>
3. #include <unistd.h>
4. 
5. int main(void)
6. {     
7. //fork创建一个新进程
8.        pid_t p1 = -1;
9. 
10. //fork之前的操作只有父父进程才有
11.        p1 = fork();    //调用一次fork函数会返回两次
12. 
13. if(p1 == 0)
14.        {
15. //这里一定是子进程
16. //先sleep一下,让父进程先死
17.               sleep(1);
18. 
19.               printf("子进程, pid = %d.\n", getpid());
20.               printf("子进程, p1 = %d.\n", p1);
21.               printf("子进程中父进程的ID = %d.\n", getppid());
22.        }
23. if(p1 > 0)
24.        {
25. //这里一定是父进程
26.               printf("父进程, pid = %d.\n", getpid());
27.               printf("父进程, p1 = %d.\n", p1);
28.        }
29. 
30. if(p1 < 0)
31.        {
32. 
33. //这里一定是出错了
34.        }
35. 
36. //在这里的操作,父子进程都有
37. //printf("hello world, pid = %d.\n", getpid());
38. 
39. return 0;
40. }

输出结果如下

结论:

(1)fork函数调用一次会返回两次,返回值等于0的就是子进程,返回值大于0的就是父进程

(2)fork的返回值在子进程中等于0, 在父进程中返回值等于本次fork创建的子进程的PID

(3)这里在子进程中打印父进程PID,为什么跟在父进程中打印父进程的PID结果不一样?

是因为在执行的时候,父进程先执行完,然后子进程就没有父进程了;这是在子进程中打印的父进程PID就是init进程

 

程序二讲解:

1. #include <stdio.h>
2. #include <unistd.h>
3. #include <stdlib.h>
4. #include <sys/types.h>
5. 
6. int main(void)
7. {
8.        pid_t pid ;
9. //调用一次返回2次,在父进程返回子进程的PID,在子进程中返回0
10. 
11.        int n = 10;
12.        pid = fork();
13. 
14. if(pid > 0)
15.        {/*in parent*/
16.               while(1)
17.               {
18.                      printf("I am parent %d\n", n++);
19.                      printf("my pid = %d.\n", getpid());
20.                      printf("my parent pid = %d.\n", getppid());
21.                      sleep(1);
22.               }
23.        }
24. 
25. else if(pid == 0)
26.        {/*in child*/
27.               while(1)
28.               {
29.                      printf("I am child %d\n", n++);
30.                      printf("my pid = %d.\n", getpid());
31.                      printf("my parent pid = %d.\n", getppid());
32.                      sleep(3);
33. 
34.               }
35.        }
36. 
37. else
38.        {
39.               perror("fork");
40. exit(0);
41.        }
42. 
43. return 0;
44. }

(1)父子进程中n值分别独立加,说明子进程从父进程中克隆过来的数据,已经不是共享的了。两者有自己独立的空间。

(20-3G放应用层代码,子进程直接从父进程中拷贝,3-4G是内核空间

(3)读时共享,写时复制(copy on write)(好处:节省物理内存开销,省去直接复制的时间)

fork创建了子进程之后,调用exec函数执行子进程

 

exec函数

      用fork创建子进程后执行的是和父进程相同的程序,可以通过if判断pid的返回值让子进程执行不同的代码分支,这样设计程序不灵活。通过调用exec函数,用新程序将子进程的用户空间代码和数据替换,直接去执行新程序。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

其实有六种以exec开头的函数,统称exec函数:

1. #include <unistd.h>
2. 
3. int execl(const char *path, const char *arg, ...);
4. 
5. int execlp(const char *file, const char *arg, ...);
6. 
7. int execle(const char *path, const char *arg, ..., char *const envp[]);
8. 
9. int execv(const char *path, char *const argv[]);
10. 
11. int execvp(const char *file, char *const argv[]);
12. 
13. int execve(const char *path, char *const argv[], char *const envp[]);

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错

则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。

 

程序1:

1. #include <stdio.h>
2. #include <unistd.h>
3. 
4. 
5. int main(void)
6. {
7. printf("hello.\n");
8. execl("/bin/ls", "ls", "-l", NULL);
9. printf("world.\n");
10. return 0;
11. }

执行结果:

分析:

可以看到没有执行      printf(“world.\n”);

这是因为/bin/ls加载到用户用户空间替换了原来代码段,所以execl下面的代码就不会执行了。所以程序是从ls的return 0退出程序的。

 

函数辨析

execl与execlp

      execl("/bin/ls", "ls", "-l", NULL);

      execlp("ls", "ls", "-l", NULL);

execl要给全执行进程的路径,而execlp除了在当前路径下找,还可以去PATH环境变量底下找。p就是去环境变量底下找,不加p就是在当前路径下找,所以要给全路径

 

execv与execvp

这两个函数的功能跟excel与execlp的功能一样,只是将参数列表以数组指针的方式给出。

还是用上面执行ls举例

char *argvv[] = {“ls”, “-l”, NULL};

execv(“/bin/ls”, argvv);                execvp(“ls”, argvv);

 

execle execve中添加了一个替换环境变量的参数,在加载新的程序的时候,我们可以不用原来的环境变量。(这两个函数实际用的很少)

 

wait & waitpid

僵尸进程

(1)子进程先于父进程结束。子进程结束后父进程此时并不一定立即就能帮子进程“收尸”,在这一段(子进程已经结束且父进程尚未帮其收尸)子进程就被称为僵尸进程。

(2)子进程除 task_struct 和栈外其余内存空间皆已清理

(3)父进程可以使用 wait 或 waitpid 以显式回收子进程的剩余待回收内存资源并且获取子进程退出状态。【父进程帮子进程收尸是要调用函数的】

(4)父进程也可以不使用 wait 或者 waitpid 回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源。(这样设计是为了防止父进程忘记显式调用 wait/waitpid 来回收子进程从而造成内存泄漏)

 

孤儿进程

(1)父进程先于子进程结束,子进程成为一个孤儿进程。

(2)linux 系统规定:所有的孤儿进程都自动成为一个特殊进程(进程 1,也就是 init 进程)的子进程。

1. #include <sys/types.h>
2. 
3. #include <sys/wait.h>
4. 
5. pid_t wait(int *status);
6. 
7. pid_t waitpid(pid_t pid, int *status, int options);

wait返回值:成功回收子进程,返回终止的子进程的pid,错误返回-1

(1)waitpid参数含义:

参数值

含义

pid<-1

等待进程组号为pid绝对值的任何子进程。

pid=-1

等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。

pid=0

等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。

pid>0

等待进程号为pid的子进程。

(2)int *status

这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会退出,是正常退出还是出了什么错误。如果status不是空指针,则状态信息将被写入寄存器指向的位置。当然,如果不关心子进程为什么推出的话,也可以传入空指针。

Linux提供了一些非常有用的宏来帮助解析这个状态信息,这些宏都定义在sys/wait.h头文件中。主要有以下几个:

说明

WIFEXITED(status)

如果子进程正常结束,它就返回真;否则返回假。

WEXITSTATUS(status)

如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。

WIFSIGNALED(status)

如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。

WTERMSIG(status)

如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。

WSTOPSIG(status)

如果当前子进程被暂停了,则返回真;否则返回假。

WIFSTOPPED(status)

如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。

(3)int options

参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。主要使用的有以下两个选项:

参数

说明

WNOHANG

如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。

WUNTRACED

如果子进程进入暂停状态,则马上返回。

这些参数可以用“|”运算符连接起来使用。

如果waitpid()函数执行成功,则返回子进程的进程号;如果有错误发生,则返回-1,并且将失败的原因存放在errno变量中。

失败的原因主要有:没有子进程(errno设置为ECHILD),调用被某个信号中断(errno设置为EINTR)或选项参数无效(errno设置为EINVAL)

如果像这样调用waitpid函数:waitpid(-1, status, 0),这此时waitpid()函数就完全退化成了wait()函数。

 

程序1

1. #include <stdio.h>
2. #include <unistd.h>
3. #include <stdlib.h>
4. #include <sys/types.h>
5. #include <wait.h>
6. 
7. int main(void)
8. {
9.        pid_t pid, pid_c;
10. //调用一次返回2次,在父进程返回子进程的PID,在子进程中返回0
11. 
12.        int n = 10;
13.        pid = fork();
14. 
15. if(pid > 0)
16.        {/*in parent*/
17.               while(1)
18.               {
19.                      printf("I am parent pid = %d\n", getpid());
20.                      pid_c = wait(NULL);            //wait是一个阻塞函数,等待回收子进程资源
21. 
22. //如果没有子进程,wait返回-1
23.                      printf("wait for child %d.\n", pid_c);
24.                      sleep(1);
25.               }
26.        }
27. 
28. else if(pid == 0)
29.        {/*in child*/
30. //printf("I am child %d\n", n++);
31.               printf("I am child pid = %d.\n", getpid());
32. //printf("my parent pid = %d.\n", getppid());
33.               sleep(10);
34.        }
35. 
36. else
37.        {
38.               perror("fork");
39. exit(0);
40.        }
41. 
42. return 0;
43. }

编译之后,程序执行结果如下:

刚进来的时候打印一次父子进程的ID号,然后子进程slepp 10s,父进程执行wait等待回收子进程,阻塞在这里,直到子进程结束。子进程结束之后,wait的返回值就是-1.

僵尸进程对内存的影响:子进程如果没有回收,子进程占了一个PCB,浪费内存,造成内存泄露。

 

程序2:

1. #include <stdio.h>
2. #include <sys/types.h>
3. #include <unistd.h>
4. #include <wait.h>
5. 
6. 
7. int main(void)
8. {
9. 
10.        pid_t pid;
11. 
12.        int n = 3;
13. 
14.        while(n--)
15.        {
16.               pid = fork();
17. if(pid == 0) break;
18.        }
19. 
20. if(pid == 0)
21.        {
22. //while(1)
23.               {
24.                      printf("I am child %d.\n", getpid());
25.                      sleep(3);
26.               }
27.        }
28. 
29. else if(pid > 0)
30.        {
31. 
32.               pid_t pid_c;
33.               while(1){
34. 
35. //pid_c = wait(NULL);
36.                      printf("I am parent.\n");
37.                      pid_c = waitpid(0, NULL, WNOHANG);
38. 
39. if(pid_c == -1)     
40. continue;
41. else
42.                             printf("wait for child %d.\n", pid_c);
43. if(pid_c > 0){
44.                             n++;     
45.                      }
46. if(n == 2)
47.                             break;                  
48.                      sleep(1);
49.               }
50.        }
51. 
52. return 0;
53. }

一个父进程fork了三个子进程,通过ps – ajx查看

 

可以看到三个子进程的pid号依次递增,,父进程与三个子进程同属于一个进程组。(父进程创建子进程之后,子进程跟父进程默认是一个组)

如何kill掉一个进程组,命令【kill -9 -14943】

注释掉while(1),执行之后可以看到,通过WNOHANG确实将waitpid设置成了非阻塞的。

目录
打赏
0
0
0
0
20
分享
相关文章
Linux 主要应用领域的归纳
服务器领域 Linux在服务器领域的应用是其最为广泛和成熟的领域之一。由于其开源、稳定、高效和安全的特性,Linux成为许多企业服务器的首选操作系统。 Web服务器:Linux是Web服务器的理想选择,因为它支持Apache、Nginx等流行的Web服务器软件,这些软件在Linux上运行稳定且性能卓越。Linux服务器可以高效地处理大量并发请求,提供快速、可靠的Web服务。 数据库服务器:Linux也广泛用于数据库服务器,如MySQL、PostgreSQL和Oracle等数据库管理系统在Linux上运行良好。Linux的稳定性和安全性使得它成为存储和管理敏感数据的理想平台。 邮件服务器:Lin
48 5
|
2月前
|
Linux systemd 服务启动失败Main process exited, code=exited, status=203/EXEC
通过以上步骤,可以有效解决 systemd 服务启动失败并报错 `Main process exited, code=exited, status=203/EXEC` 的问题。关键在于仔细检查单元文件配置、验证可执行文件的有效性,并通过日志分析具体错误原因。确保可执行文件路径正确、文件具有执行权限,并且可以独立运行,将有助于快速定位和解决问题。
569 7
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
134 13
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
72 5
Linux系统在服务器领域的应用与优势###
本文深入探讨了Linux操作系统在服务器领域的广泛应用及其显著优势。通过分析其开源性、安全性、稳定性和高效性,揭示了为何Linux成为众多企业和开发者的首选服务器操作系统。文章还列举了Linux在服务器管理、性能优化和社区支持等方面的具体优势,为读者提供了全面而深入的理解。 ###
Linux手账—exec和fork
本文介绍了Linux系统中进程控制的核心功能——`fork`和`exec`系列函数。`fork`用于创建新进程(子进程),继承父进程的资源但拥有独立的地址空间;`exec`系列函数则在当前进程中执行新程序,替换原有地址空间。文章详细解析了这些函数的基本概念、用法及工作原理,强调了它们在多进程编程中的重要性。
86 0
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决
如果遇到"exec format error"问题,文章建议先检查Linux操作系统是32位还是64位,并确保安装了与系统匹配的JDK版本。如果系统是64位的,但出现了错误,可能是因为下载了错误的JDK版本。文章提供了一个链接,指向Oracle官网上的JDK 17 Linux版本下载页面,并附有截图说明。
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决
揭秘Python IPC:进程间的秘密对话,让你的系统编程更上一层楼
【9月更文挑战第8天】在系统编程中,进程间通信(IPC)是实现多进程协作的关键技术。IPC机制如管道、队列、共享内存和套接字,使进程能在独立内存空间中共享信息,提升系统并发性和灵活性。Python提供了丰富的IPC工具,如`multiprocessing.Pipe()`和`multiprocessing.Queue()`,简化了进程间通信的实现。本文将从理论到实践,详细介绍各种IPC机制的特点和应用场景,帮助开发者构建高效、可靠的多进程应用。掌握Python IPC,让系统编程更加得心应手。
60 4
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等