Linux之进程控制

简介: Linux之进程控制

前言

本文介绍了进程终止、进程等待、进程替换等进程控制的相关概念。


一、再识fork

我们在之前的内容中已经了解过系统调用fork,今天我们再来深入了解一下fork。

  1. 我们已经知道了fork有两个返回值,但是为什么会出现这种现象呢?
    fork函数的实现在操作系统内部,当这个函数准备返回时,它的核心代码已经执行完了也就是子进程已经被创建了,并且已经在OS的运行队列中准备被调度了。
    fork后,有两个执行流,父子进程是共享的,所以return会被父子进程各自调度一次,也就是调度两次。

  2. 为什么给父进程返回子进程的pid,给子进程返回0呢?
    子进程的父进程只有一个,而父进程可以有很多个子进程,这是一个现实的事情,一个孩子只有一个父亲,而一个父亲可以有很多个孩子。
    那么给父进程返回子进程的pid,给子进程返回0,可以很方便的区分父子进程。
  3. 同一个变量,为什么会有两个不同的值呢?
    返回的本质是写入,谁先返回,就是谁先写入,因为进程具有独立性,因此会发生写时拷贝。变量的地址相同但是内容不同。
  4. fork的使用场景
    (1)父进程希望子进程执行一部分父进程的代码,用if和else if将父进程和子进程的代码分隔开(现实:父亲希望子女继承家业,成为自己的帮手);
    (2)父进程希望子进程执行另一个程序,用程序替换实现(现实:孩子要闯出属于自己的天地)。
  5. fork失败
    (1)系统中有太多的进程;
    (2)实际用户的进程数超过了限制。

二、退出码

退出码是用来标识一个进程任务执行结果的情况。因为成功只有一种情况,而失败的情况很多,因此,一般情况下0表示执行成功,非0表示执行失败。非0的数字不同,所表示的错误不同。系统对于退出码一般都有着相应的文字藐视,当然我们也可以自定义,也可以直接使用系统给定的映射关系。(例如,strerror这个函数)

文件test4.c

1 #include<stdio.h>
  2 #include<string.h>
  3 int main()
  4 {
  5         for(int i = 0;i < 200; ++i)
  6         {
  7                 printf("%d : %s\n",i,strerror(i));
  8         }
  9         return 0;
 10 }

运行:

可以看到strerror函数有134种系统映射好的文字描述的错误码。

三、进程终止

1.进程终止的情况

  • 代码运行完,并且结果正确;
  • 代码运行完,结果不正确;
  • 代码未运行完,进程异常结果无意义。

2.进程退出的方法

  • 从主函数(main函数)return返回;
  • 程序的任意位置调用exit(code);
  • 系统调用_exit(code);

3._exit()和exit()

_exit()和exit()不同:

  • _exit是系统调用;exit是库函数,是对系统调用的上层封装。
  • _exit终止进程,不会刷新缓冲区;exit终止进程,会刷新缓冲区。
  • 调用exit:
    文件test4.c
1 #include<stdio.h>
  2 #include<stdlib.h>
  3 int main()
  4 {
  5         printf("hello world!");
  6         sleep(1);
  7         exit(1);
  8         return 0;
  9 }

  • 调用_exit()
    文件test4.c
1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main()
  5 {
  6         printf("hello world!");
  7         sleep(1);
  8         _exit(1);
  9         return 0;
 10 }

同样的代码,调用exit和调用_exit终止进程后,得到的结果却不同。exit会主动刷新缓冲区,而_exit不会主动刷新缓冲区,这是因为缓冲区所在的位置。

四、进程等待

1.进程等待的场景

之前讲进程状态的时候,我们讲到一种特殊的转态:僵尸状态:子进程退出,父进程没有等待子进程(没有回收子进程的资源),会造成子进程处于僵尸状态,造成内存泄漏,同时这种状态的进程无法被杀死(甚至是kill -9都不能杀死这个进程,因为他已经死了)。为了避免这种情况,为了让父进程知道子进程的任务完成的怎么样,我们需要让父进程通过进程等待的方式,回收子进程退出后的资源。

2.进程为什么要等待

1.父进程通过进程等待的方式回收子进程的资源;

2.父进程要获取子进程的退出信息

3.进程等待的方式

wait

**参数:**输出型参数,一般设置为status,用于获取进程的退出状态,如果不关心进程的退出状态可以设置为NULL;

**返回值:**wait成功返回等待的子进程pid,等待失败返回-1。

waitpid

参数

  1. pid,设置为-1时,表示等待任意一个进程,和wait一样;也可以指定等待的进程pid,就只能等待指定的进程。
  2. status,输出型参数,由操作系统填充进程退出的信息,如果传递NULL说明不关心该信息。
    status不能当作一个简单的整数对待,而应该被当作一个位图看待:

    它的次低8位表示它的退出状态(退出码)((status >> 8) & 0xff);
    低7位表示终止信号(&0x7f)。
  3. 设置为0时表示,阻塞式等待进程;设置为WNOHANG时,表示非阻塞式等待进程。

返回值:如果等待成功,返回所等待的进程pid;如果等待失败,则返回0。

进程等待的信息存在哪里

子进程和父进程都有各自对应PCB,当子进程退出且父进程未回收子进程资源时,子进程的退出信息以及子进程的资源信息都保存在子进程的PCB中。waitpid系统调用,以操作系统的身份执行代码,找到子进程,将输出型参数status参数传到子进程内部,把子进程的退出信息设置进status中,再将status取出,父进程就获得了子进程的退出信息。

补充

对于获取进程的退出结果,我们可以不采用对输出型参数进行位操作,因为Linux提供了对应操作的宏。

WIFEXITED(status):若子进程为正常终止,返回的状态,则为真(查看进程是否正常退出)

WEXITSTATUS(status):若WIFEXITED为非0,则用它提取子进程的退出码(查看进程的退出码)

五、进程替换

进程替换,就是在原进程执行到一定时间是,将原进程替换成磁盘中指定的一个新的进程,然后去执行新的进程(原进程剩余部分不会执行)。

1.进程函数

注意:对于execl系列函数的参数要以NULL作为结尾(参数是可以变参数)

exec系列函数的返回值,如果替换成功,则没有返回值,而替换失败则返回-1。

2.演示

execl函数:

文件mytest.c

1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5         printf("process is running...\n");
  6         execl("/usr/bin/ls","ls","--color=auto","-a","-l",NULL);
  7         printf("process running done!");
  8         return 0;
  9 }

其他

其他函数的用法:

l(list):表示参数采用列表;

v(vector):表示参数采用数组(将所有执行的参数传入数组中进行统一传递,不用使用可变参数);

p(path):自动搜索环境变量(PATH);

e(env):表示自己维护环境变量。

execlp

文件mytest.c

1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5         printf("process is running...\n");
  6         execlp("ls","ls","--color=auto","-a","-l",NULL);
  7         printf("process running done!");
  8         return 0;
  9 }

execv

文件mytest.c

1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5         printf("process is running...\n");
  6         char* const argv[] = {
  7                 "ls",
  8                 "--color=auto",
  9                 "-a",
 10                 "-l",
 11                 NULL
 12         };
 13         execv("/usr/bin/ls",argv);
 14         printf("process running done!");
 15         return 0;
 16 }

execvp

文件mytest.c

1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5         printf("process is running...\n");
  6         char* const argv[] = {
  7                 "ls",
  8                 "--color=auto",
  9                 "-a",
 10                 "-l",
 11                 NULL
 12         };
 13         execvp("ls",argv);
 14         printf("process running done!");
 15         return 0;
 16 }

execle

文件myprocess.c

1 #include<stdio.h>
  2 #include<stdlib.h>
  3 int main()
  4 {
  5         printf("PATH:%s\n",getenv("PATH"));
  6         printf("PWD:%s\n",getenv("PWD"));
  7         printf("MYENV:%s\n",getenv("MYENV"));
  8         printf("我是一个进程\n");
  9         printf("我是一个进程\n");
 10         printf("我是一个进程\n");
 11         printf("我是一个进程\n");
 12         return 0;
 13 }

文件mytest.c

1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5         printf("process is running...\n");
  6         char* const envi[] = {
  7                 (char*)"MYENV=123454",
  8                 NULL
  9         };
 10         execle("./myprocess","myprocess",NULL,envi);
 11         printf("process running done!");
 12         return 0;
 13 }

execve

只有它是系统调用,其他exec系列的函数都是基于它做的封装,目的是为了有更多的应用场景,有更多的选择

3.替换原理

为什么调用替换函数后,原程序的剩余部分不再执行?

用fork创建子进程后,执行的可能是父进程相同的进程(也可能执行与父进程不同道德代码分支),子进程往往会调用exec系列的函数以执行另一个程序。

当进程调用exec系列函数时,该进程的用户空间的代码和朱家具完全被新程序替换,从新程序的启动例程开始执行。

要注意,调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

简单来说,程序替换的本质时将指定的程序的代码和数据加载到指定的位置,覆盖原本的代码和数据(即,进程替换时并没有创建新的进程)。

因此,原进程后续的代码会被新进程的代码和数据所覆盖,因此就不会执行剩余代码了。

这也是为什么只有当程序替换失败才会有返回值,因为如果替换成功就不会执行接下来的代码,返回值就无意义。


总结

以上就是今天要讲的内容,本文介绍了进程终止、进程等待、进程替换等的相关概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。

最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

相关文章
|
20天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
45 1
|
2月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
49 0
|
9天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
61 13
|
16天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
23天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
1月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
146 4
linux进程管理万字详解!!!
|
29天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
1月前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
85 8
|
1月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
92 1
|
1月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?