【Linux】进程控制(进程创建、进程终止、进程等待、进程替换)
文章目录
- 一、进程创建
- 1.1 系统调用 fork
- 1.2 理解 fork 的返回值
- 1.3 写时拷贝策略
- 二、进程终止
- 2.1 main 函数的返回值
- 2.2 进程退出的几种情况
- 2.3 进程退出码
- 2.4 终止正常进程:return、exit、_exit
- 2.5 站在 OS 角度:理解进程终止
- 三、进程等待
- 3.1 进程等待的必要性
- 3.2 如何「进程等待」:wait、waitpid 函数
- 3.1.1 wait 函数
- 3.1.2 waitpid 函数 ⭐
- ① status 参数:
- 1、获取子进程的退出码
- 2、获取子进程的终止信号
- 3、代码实现:一个完整的进程等待 ⭐
- ② options 参数:
- waitpid 的两种等待方式:阻塞 & 非阻塞
- 补充:如何理解阻塞/等待
- 补充:内核源码中的退出码和终止信号
- 3.1.3 总结
- 四、进程的程序替换
- 4.1 前言
- 4.2 替换原理
- 4.3 如何替换:exec 系列函数
- execl 函数
- execv 函数
- execlp 函数
- execle 函数(用的很少)
一、进程创建
目前学习到的进程创建的两种方式:
- 命令行启动命令(程序、指令等) 。
- 通过程序自身,调用 fork 函数创建出子进程。
1.1 系统调用 fork
Linux 中的系统接口 fork 函数是非常重要的函数,它从已存在进程(父进程)中创建一个新进程(子进程):
#include<unistd.h>
pid_t fork(void); // 返回值:子进程中返回0,父进程中返回子进程id,出错返回-1
进程调用 fork,当控制转移到内核中的 fork 函数代码后,操作系统内核会做:
- 分配新的内存块和内核数据结构(task_struct)给子进程。
- (以父进程为模板)将父进程的内核数据结构中的部分内容拷贝至子进程。
- 添加子进程到系统进程列表当中(因为进程要被调度和执行)。
- fork 函数返回后,开始调度器调度。
fork 的常规用法:
一个父进程希望复制自己,使父子进程同时执行不同的代码段。
例如:父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。
例如:子进程从 fork 返回后,调用 exec 函数。
fork 调用失败的原因:
系统中有太多的进程,系统资源不足。
实际用户的进程数超过了限制。
1.2 理解 fork 的返回值
当一个进程调用 fork 之后,在不写入的情况下,用户的代码和数据是父子进程共享的。就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
#include<stdio.h> // perror
#include<unistd.h> // getpid, getppid, forkint main()
{ // ...pid_t ret = fork(); // 返回时发生了写时拷贝if (ret == 0) {// child processwhile (1) {printf("child process, pid:%u, ppid:%u\n", getpid(), getppid());sleep(1);}}else if (ret > 0) {// father processwhile (1) {printf("father process, pid:%u, ppid:%u\n", getpid(), getppid());sleep(1);}}else {// failureperror("fork");}return 0;
}
fork 之前父进程独立执行,fork 之后父子进程分别执行。注意:fork 之后谁先执行完全由调度器决定。
画图理解 fork 函数:
思考:
为什么 fork 有两个返回值,从而使父子进程进入不同的业务逻辑。为什么 fork 的返回值会返回两次呢?
fork 函数中的 return 语句是被父子进程共享的,所以都会被父子进程执行。当 frok 返回时,会往变量 ret 中写入数据(如:
pid_t ret = fork();
),发生了写时拷贝,导致 ret 有两份,分别被父子进程私有。(代码共享,数据各自私有)返回值 ret 变量名相同,为什么会有两个不同的值呢?
变量名相同,有两个不同的值,本质是因为被映射到了不同的物理地址处。
1.3 写时拷贝策略
写时拷贝是一种延时操作的策略,为什么要有写时拷贝呢?写时拷贝的好处是什么?
- 为了保证父子进程的独立性!(数据各自私有一份)
- 不是所有的数据,都有必要被拷贝一份(比如只读的数据)。写时拷贝可以节约资源。
- fork 时,如果把所有的数据都拷贝一份,是需要花费时间的,降低了效率。写时拷贝可以提高 fork 执行的效率。
- fork 创建子进程本身就是向操作系统要资源,如果把所有的数据都拷贝一份,要更多的资源,更容易导致 fork 失败。写时拷贝可以减少 fork 失败的概率。
二、进程终止
2.1 main 函数的返回值
我们在写 C/C++ 代码时,main 函数里面我们总是会返回 0,比如:
#include<stdio.h>
int main()
{printf("hello world\n");return 0;
}
思考:为什么 main 函数中总是会返回 0 ( return 0;
)呢?
- main 函数中的这个返回值叫做:「进程退出码」,用来表示进程退出时,其执行结果是否正确。
- 返回的 0 是给操作系统看的,来确认进程的执行结果是否正确。(0 通常表示成功)
用户可以通过命令 echo $?
查看最近一次执行的程序的「进程退出码」,比如:
[ll@VM-0-12-centos 12]$ ./test
hello world
[ll@VM-0-12-centos 12]$ echo $? # 查看最近一次执行的程序的退出码
0
2.2 进程退出的几种情况
- 代码跑完,结果正确。(退出码:0)
- 代码跑完,结果不正确。(一般是代码逻辑有问题,但没有导致程序崩溃,退出码:非0)
- 代码没跑完,程序非正常终止了。(这种情况下,退出码已经没有意义了,是由信号来终止,比如 ctrl+c)
2.3 进程退出码
父进程创建子进程的目的是为了让子进程给我们完成任务,父进程需要通过「子进程的退出码」知道子进程把任务完成的怎么样。
比如在生活中,网页打不开时,用户需要通过返回的一串错误代码得知网页出错的原因:
退出码可以人为的定义,也可以使用系统的错误码列表(错误码 (int) 与错误码描述 (string) 之间的映射表)
比如:C 语言库中提供一个接口,可以把「错误码」转换成对应的「错误码描述」,程序如下:
#include<stdio.h>
#include<string.h> // strerrorint main()
{for (int i = 0; i < 10; i++) {printf("%d -- %s\n", i, strerror(i)); // char *strerror(int errnum);} return 0;
}
运行结果:
[ll@VM-0-12-centos 12]$ ./test
0 -- Success
1 -- Operation not permitted
2 -- No such file or directory
3 -- No such process
4 -- Interrupted system call
5 -- Input/output error
6 -- No such device or address
7 -- Argument list too long
8 -- Exec format error
9 -- Bad file descriptor
2.4 终止正常进程:return、exit、_exit
注意:⭐
- 只有 main 函数中的 return 表示的是终止进程,非 main 函数中的 return 不是终止进程,而是结束函数。
- 在任何函数中调用 exit 函数,都表示直接终止该进程。
库函数:exit:
#include <stdlib.h>
void exit(int status); // 终止正常进程
// 参数 status: 定义了进程的终止状态,父进程通过 wait 函数来获取该值
系统调用:_exit:
#include <unistd.h>
void _exit(int status); // 终止正在调用的进程
系统调用接口 _exit
的功能也是终止正在调用的进程,它和库函数 exit
有什么区别呢?
- exit:在进程退出的时候,会进行后续资源处理(比如刷新缓冲区)。
- _exit:在进程退出的时候,不会进行后续资源处理,直接终止进程。
补充:
其实,库函数 exit
最后也会调用系统接口 _exit
,但在调用 _exit
之前,还做了其他工作:
- 执行用户通过 atexit 或 on_exit 定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用
_exit
。
2.5 站在 OS 角度:理解进程终止
站在操作系统角度,如何理解进程终止?
“ 释放 ” 曾经为了管理该进程,在内核中维护的所有数据结构对象。
注意:这里的 “ 释放 ” 不是真的把这些数据结构对象销毁,即占用的内核空间还给 OS;而是设置成不用状态,把相同类型的对象归为一类(如进程控制块就是一类),保存到一个 “ 数据结构池 ” 中,凡是有不用的对象,就链入该池子中。
我们知道在内核空间中维护一个内存池,减少了用户频繁申请和释放空间的操作,提高了用户使用内存的效率,但每次从内存池中申请和使用一块空间时,还需要先对这块空间进行类型强转,再初始化。
现在有了这些 “ 数据结构池 ” ,比如:当创建新进程时,需要创建新的 PCB,不需要再从内存池中申请一块空间,进行类型强转并初始化,而是从 “ 数据结构池 ” 中直接获取一块不用的 PCB 覆盖初始化即可,减少了频繁申请和释放空间的过程,提高了使用内存的效率。
这种内存分配机制在 Linux 中叫做 slab 分配器。
释放程序代码和数据占用的内存空间。
注意:这里的释放不是把代码和数据清空,而是把占用的那部分内存设置成「未使用」就可以了。
取消曾经该进程的链接关系。
三、进程等待
3.1 进程等待的必要性
子进程退出,父进程还在运行,但父进程没有读取到子进程状态,就可能造成「僵尸进程」的问题,进而导致内存泄漏。
退出状态本身要用数据维护,也属于进程的基本信息,所以保存在 task_struct(PCB) 中,换句话说,僵尸进程一直不退出,它对应的 PCB 就要一直维护。
另外,进程一旦变成僵尸状态,命令
kill -9
也无能为力,因为没有办法杀死一个已经死去的进程。最后,父进程需要知道派给子进程的任务完成的如何。(如:子进程运行完成,运行结果对不对,有没有正常退出,还有根据进程退出信息制定出错时的一些策略)
思考:为什么要有进程等待?
等待子进程终止,回收僵尸进程,从而解决内存泄露问题。
获取子进程的退出信息。—— 不是必须的,需要就获取,不需要就不获取。
因为父进程需要知道派给子进程的任务完成的如何,有没有正常退出,还可以根据进程退出信息制定出错时的一些策略。
尽量保证父进程要晚于子进程退出,可以规范化的进行资源回收。—— 这是编码方面的要求,并非系统。
…
总结:父进程通过进程等待的方式:回收子进程资源,防止内存泄漏;获取子进程的退出信息。⭐
3.2 如何「进程等待」:wait、waitpid 函数
系统调用 wait
,waitpid
- 等待任意一个子进程改变状态,子进程终止时,函数才会返回。(其实就是等待进程由 R/S(运行/睡眠) 状态变成 Z(僵尸) 状态,然后父进程读取子进程的状态,操作系统回收子进程)
3.1.1 wait 函数
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
/*
* wait() 系统调用:暂停正在调用进程的执行,直到它的一个子进程终止。
* 调用 wait(&status) 等价于 waitpid(-1, &status, 0);
*/
参数:
- status:输出型参数,获取子进程退出状态,不关心则可以设置成为 NULL。
返回值:
- 成功时,返回终止子进程的进程 ID,出错时,返回 -1。
【Linux】进程控制(进程创建、进程终止、进程等待、进程替换)相关推荐
- Linux——进程控制:创建、终止、等待、替换
进程创建 fork #include <unistd.h> pid_t fork(void); 操作系统做了什么? 调用fork之后,内核的工作: 分配新的内存块和内核数据结构给子进程 将 ...
- linux——进程(创建、终止、等待、替换)
进程的基本操作 概念 程序运行的一个实例,其占有一定的空间. 查询某一进程当前情况 ps aux | grep 进程名 终止进程 kill -9 pid: //pid指需要终止的进程pid 创建 pi ...
- 【Linux】Linux进程控制 --- 进程创建、终止、等待、替换、shell派生子进程的理解…
柴犬: 你好啊,屏幕前的大帅哥or大美女,和我一起享受美好的今天叭
- 【Linux】进程控制(创建、终止、等待)
环境:centos7.6,腾讯云服务器 Linux文章都放在了专栏:[Linux]欢迎支持订阅 相关文章推荐: [Linux]冯.诺依曼体系结构与操作系统 [Linux]进程理解与学习Ⅰ-进程概念 [ ...
- 进程控制-创建、退出、等待、替换
目录 进程创建 1.子进程继承 2.写时拷贝 进程退出 echo $? 退出码 进程异常退出的情况模拟: 退出进程的方式 退出码的意义: 进程退出,在系统中发生了什么? 进程等待 为什么要有进程等待呢 ...
- MFC关于进程使用:创建、关闭及查询进程
// 启动进程 bool StartProgress(CString& strError) {CString strExeName;strExeName.Format(_T("%s& ...
- Linux系统编程36:多线程之线程控制之pthread线程库(线程创建,终止,等待和分离)
文章目录 (1)POSIX线程库 (2)pthread_create--创建线程 A:关于Linux线程的再理解 B:线程ID及地址空间布局 (3)pthread_exit--线程终止 (4)pthr ...
- Linux_进程控制(创建进程,等待进程,进程终止)
文章目录 1.创建进程 1.1 fork()函数初识 1.2 fork()创建进程代码示例 2.等待进程 2.1 进程等待概念 2.2进程等待必要性 2.3 进程等待方法 2.3.1 wait 2.3 ...
- (王道408考研操作系统)第二章进程管理-第一节3:进程控制(配合Linux讲解)
文章目录 一:如何实现进程控制 二:进程控制原语 (1)进程创建 A:概述 B:补充-Linux中的创建进程操作 ①:fork() ②:fork()相关问题 (2)进程终止 A:概述 B:补充-僵尸进 ...
- Linux进程控制(一)
文章目录 进程创建 fork函数进一步探讨 写时拷贝 进程终止 进程退出场景 进程终止时,操作系统做了什么? 三大终止进程函数 进程等待(阻塞) 进程等待的必要性 进程等待的两种函数 获取子进程参数s ...
最新文章
- 人脸检测识别文献代码
- PaddlePaddle 中的若干基础命令中的问题
- 最好的浏览器排行榜_PG是最好的数据库;TiDB 4.0前瞻;SequoiaDB高可用原理;20c DG新特性... 数据库周刊第18期...
- swift与OC之间不得不知道的21点
- C++什么是内存泄漏
- 206块积木,72套进阶玩法!玩转STEAM教育,帮你省掉上万块的乐高课
- uint8 转换为 float
- c语言计算机猜数字100以内,求一个猜数字C语言代码,要求如下 计算机生成一个100以内的随机数,玩家来猜 记录猜的次数,最后打...
- dsd语言证书c1是什么,DSD一级德语语言证书考试在嘉兴高级中学举行
- CSDN西安分站俱乐部聚会归来记
- 204页数字化转型:集团企业信息化规划方案
- Max Script|加密写法
- APP中使用UI交互设计动效的三个好处
- 第一章概述-------第一节--1.1计算机网络在信息时代中的作用
- jMonkeyEngine译文 FlagRush1——通过SimpleGame创建你的第一个应用程序
- 国庆节,一天开发一个小程序+Web系统。2.5K到手。【分享开发经验】【收藏起来】
- VSCode更新失败无法打开,快捷方式无法正常工作
- MAC 部分目录作用
- 小程序FMP优化实录,已拿offer附真题解析
- HGAME 2022 WEB