Linux 进程控制

目录

前言

fork函数

1.进程创建:fork返回值问题

2.进程终止

进程退出的情况

3.进程等待

4.进程程序替换(重要)

1.创建子进程的目的

2.进程程序替换的原理

[3. 其他的exec*函数](#3. 其他的exec*函数)

5.手撕简单命令行解释器


前言

本篇是我们的进程控制相关内容专题,等我们学完了这些进程控制内容再结合前面的内容,我们就可以制作一个简单的命令行解释器啦!

fork函数

先来了解一个函数fork(),其实我们前面也有用过,我们在这就深入去了解一下

是在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程

进程调⽤fork,当控制转移到内核中的fork代码后,内核做:

• 分配新的内存块和内核数据结构给⼦进程

• 将⽗进程部分数据结构内容拷⻉⾄⼦进程

• 添加⼦进程到系统进程列表当中

• fork返回,开始调度器调度

1.进程创建:fork返回值问题

fork函数返回值

• ⼦进程返回0

• ⽗进程返回的是⼦进程的pid

1.如何理解fork函数有两个返回值?

fork函数在库中实现的主要步骤:

a.创建子进程的PCB 赋值

b.创建子进程的地址空间 赋值

c.创建并设置页表

d.子进程放入进程list

e. ......

return pid;

当一个函数准备return时,它内部的核心代码已经执行完了,说明子进程早已经被创建好了,并且可能在操作系统的运行队列中准备被调度了;所以在内部没返回时就已经有两个执行流了,父子进程共享代码,return pid就被共享了,所以会有两个返回值(父进程和子进程各自执行return)

2.如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?

答:因为子进程的父进程只有一个,不需要知道父进程的id,而父进程的子进程可能有多个,所以需要知道子进程的id

3.如何理解同一个id值,怎么可能会保存两个不同的值,让if else if同时执行

答:pid_r id = fork();返回的本质就是写入,所以谁先返回,谁就先写入id,而后来的进程因为进程具有独立性,会触发写时拷贝,所以同一个id,地址是一样的,但是内容却不一样

再次认识写时拷贝:通常,父子进程代码共享,父子进程再不写入时数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本

fork常规用法:

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求

2.一个进程要执行一个不同的程序,例如子进程从fork返回后,调用exec函数

fork调用失败的原因:

1.系统中有太多的进程

2.实际用户的进程数超过了限制

2.进程终止

./mytest ------ 运行一个进程

echo ? ------ ?:永远记录最近一个进程在命令行中执行完毕时对应得退出码

(main -> return ? ;)

写代码是为了完成某件事情,我如何得知我的任务跑的如何呢?

答:通过进程退出码来判断

进程退出时,对应的退出码来标定进程执行的结果是否正确

退出码:

1.意义:0:success,!0:标识失败,!0具体是几,来标识不同的错误

但是数字对人不友好,对计算机友好,我们没法单通过数字来判断到底问题出在哪里,所以一般而言,退出码都必须要有对应的文字描述:

1.可以自定义

2.可以使用系统的映射关系(不常用)

系统给的部分错误码含义

2.如何设定main函数返回值呢?如果不关心进程退出码,return 0就行(一般用0表示成功,用非0表示错误),如果未来我们是要关心进程退出码得时候,要返回特定的数据表明特定的错误

进程退出的情况

1.代码跑完了,结果正确 ------return 0;

2.代码跑完了,结果不正确 ------return !0; 退出码在这个时候起效果

3.代码没跑完,程序异常,退出码无意义

正常进程如何退出:

a.main函数return返回

b.任意地方调用 exit(code) C语言库函数,在系统调用接口之上

c. _exit();------了解 系统调用

exit终止进程,主动刷新缓冲区;_exit终止进程,不会刷新缓冲区;缓冲区位于用户级

都是终止当前进程

3.进程等待

前面我们在进程状态那里知道了Z僵尸状态是一个问题,需要我们通过进程等待的方式来解决该问题

进程等待的必要性:

需要回收子进程资源,获取子进程退出信息(为什么要进程等待的原因)

进程等待相关头文件

#include<sys/types.h>

#include<sys/wait.h>

用wait(进程等待函数)回收子进程资源:

用waitpid通过等待拿到子进程的退出结果,其中pid为对应子进程的pid,status获取对应子进程退出时的退出结果(注意:这里的status不是当成整型来看待,而是有自己的位图结构,来设置不同的值,我们是想通过status知道进程退出时是哪一种情况),option传入0表示阻塞时等待

• 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息

• 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞

• 如果不存在该子进程,则立即出错返回

关于waitpid的第二个参数status:

• wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。

• 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。

• 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。

• status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位)

\^\] 退出状态来表示结果是否正确,终止信号表示是否正常退出 子进程正常退出: ![image-20250428172427939](https://i-blog.csdnimg.cn/img_convert/114e723fc59f96d6b26fccab30788fbf.png) ![image-20250428172404918](https://i-blog.csdnimg.cn/img_convert/803ec2238a8e90884eedcf4454ec2f8c.png) 子进程异常退出 让子进程发生除0错误 ![image-20250428172652284](https://i-blog.csdnimg.cn/img_convert/8af2ba200a6801c7f6b9e72a6c1af578.png) 此时拿到的信号是8号(8号代表浮点数错误,因为除0了) ![image-20250428172725912](https://i-blog.csdnimg.cn/img_convert/8128722b7a17f157a51b26096dab949c.png) ![image-20250428172917980](https://i-blog.csdnimg.cn/img_convert/2e66a4fae1bfee5a28f76b8c634f9b18.png) **等待的本质**:检测子进程退出信息(在子进程的pcb中),并将子进程退出信息通过status拿回父进程的上下文中 再谈进程退出: 1.进程退出会变成僵尸进程------会把自己的退出结果写入到自己的task_struct中 2.wait/waitpid是一个系统调用,表示操作系统也有资格、能力去读取子进程的task_struct 综上,wait/waitpid是通过操作系统从退出子进程的task_struct中获取退出码(信息)的 **阻塞和非阻塞:** ![image-20250529162058046](https://i-blog.csdnimg.cn/img_convert/19f8ccd97d3288ebdbf5707e9bf4285c.png) 非阻塞轮旋等待------父进程一直在询问子进程退出没有 ![image-20250429182254107](https://i-blog.csdnimg.cn/img_convert/43a0b475f06a4f9709fbea513e7d7ed7.png) 非阻塞有什么好处? 答:不会占用父进程的所有精力,可以在轮询期间干干别的事情 ![image-20250429190048046](https://i-blog.csdnimg.cn/img_convert/21293d5c4c5d0d309bd7c9d590f79a77.png) ## 4.进程程序替换(重要) ### 1.创建子进程的目的 a.想让子进程执行父进程代码的一部分(执行父进程对应磁盘代码的一部分) b.想让子进程执行一个全新的程序(让子进程想办法加载到磁盘上指定的程序,执行新程序的代码和数据)------ **进程的程序替换** **替换原理:⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变** ![image-20250529162143738](https://i-blog.csdnimg.cn/img_convert/5b188456fb22a276a4a4872b6ae59137.png) 替换函数: **int execl(const char\* path,const char\* arg,...); ------将指定的程序加载到内存中,让指定进程执行;第一个参数是用于找到该程序,第二个参数是表示如何执行(你在命令行中怎么执行,就怎么传参),第三个...为可变参数列表,可传多种参数** ![image-20250429202353983](https://i-blog.csdnimg.cn/img_convert/4711af625904038eb59b2430302ce8d7.png) ![image-20250429202607321](https://i-blog.csdnimg.cn/img_convert/ead98e376aae6554b8774be4cb933c26.png) > 执行ls **可以让我们的程序去用c把别人的程序调用起来------程序替换** ### 2.进程程序替换的原理 a.程序替换的本质,就是将指定的代码和数据加载到指定的位置(覆盖自己的代码和数据) b.进程替换的时候没有创建新的进程 所以在exec\*类函数成功执行后,后面的代码已经被覆盖了,不会执行 ![image-20250429205458047](https://i-blog.csdnimg.cn/img_convert/6a1b9aaf60c306949a63fac4061b6683.png) exec\*类函数成功执行返回不会有返回值(不需要,因为成功执行就代表和接下来的原来的代码无关了,判断返回值没有意义),执行失败会返回-1,只要返回,一定是错误了 ![image-20250429205932693](https://i-blog.csdnimg.cn/img_convert/76b8e58d4a8514ad36ce59c9b295fed1.png) 多进程时: ![image-20250429213527563](https://i-blog.csdnimg.cn/img_convert/e07d9975534775fe3f401db68c4880ad.png) ![image-20250429213601346](https://i-blog.csdnimg.cn/img_convert/5e6fab91dd5f9c54cef71b1effbc6c90.png) ![image-20250430114933885](https://i-blog.csdnimg.cn/img_convert/1cbbc67d09daf0748d31b3ebdca6f3c3.png) \[\^\] 在程序替换时,代码也可能发生写时拷贝 ### 3. 其他的exec\*函数 ![image-20250529162327148](https://i-blog.csdnimg.cn/img_convert/a67251f22b4d17c3cb2d21088fb6ac27.png) 前面的execl函数中l的意思是list:将参数一个一个的传入exec\* 1. ![image-20250430120031628](https://i-blog.csdnimg.cn/img_convert/e60aa4b17957efc68ba03106e8cdb44a.png) p:path:如何找到程序的功能;带p字符的函数,不需要告诉我程序的路径,只需要告诉我是谁,我会自动在环境变量PATH,进行可执行程序的查找 ![image-20250430120718731](https://i-blog.csdnimg.cn/img_convert/22e482a7acb69a7ec5f9fd7d571fa23e.png) \[\^\] 两个ls并不重复,第一个ls是告诉系统我要执行谁,第二个ls是告诉系统我想怎么执行 2. ![image-20250430125014586](https://i-blog.csdnimg.cn/img_convert/dd737124ed34804757b5a33035bd35b3.png) v:vector:可以将所有的执行参数放入数组中,统一传递,而不用进行使用可变参数方案 ![image-20250430134824175](https://i-blog.csdnimg.cn/img_convert/792049083d74e3d9416c5953a9494555.png) 前两个结合一下就是3: ![image-20250430154500785](https://i-blog.csdnimg.cn/img_convert/70dbc4ee61125b62b3004bd5cdef021f.png) **可以使用程序替换,调用任意后端语言对应的可执行程序** 4. ![image-20250430165945194](https://i-blog.csdnimg.cn/img_convert/373967d7e509f9ae4f93eea2c9e9148a.png) e:自定义环境变量 ![image-20250430181819145](https://i-blog.csdnimg.cn/img_convert/8b9e886d4568b527a7a07c9163e1e842.png) 是先执行main函数,还是先加载exec\*函数? ![image-20250430182340785](https://i-blog.csdnimg.cn/img_convert/6b628599086d91804514adce64272c1e.png) 由于我们需要先把程序加载到内存中,所以理应是exec\*函数先加载,main函数的参数数据来源就是从exec \*函数中来的 1、2、4结合起来就是5: ![image-20250430183814537](https://i-blog.csdnimg.cn/img_convert/4d524a5ea21795549345c8df74fb9160.png) 上面都是基于系统调用做的封装,为了让我们有更多的选择性 ![image-20250529162456056](https://i-blog.csdnimg.cn/img_convert/fd4e500aca89c8a36fc8193ccd9ced16.png) 真正的执行程序替换的系统调用接口为: ![image-20250430184006625](https://i-blog.csdnimg.cn/img_convert/fb8739650c4ad3e73eb73ad1be1a8205.png) ![image-20250529162539989](https://i-blog.csdnimg.cn/img_convert/2c16a0162ebc5e373fe0b0fcf0bd7cb8.png) ## 5.手撕简单命令行解释器 相关源代码可看:[myshell](https://gitee.com/mo-chu0704/linux/tree/master/myshell "myshell ") ![屏幕截图 2025-05-02 093211](https://i-blog.csdnimg.cn/img_convert/deae2307917030fc31cd11830cc01327.png) ![屏幕截图 2025-05-02 093232](https://i-blog.csdnimg.cn/img_convert/e5703dbfb40ae0608088bdb2936269cf.png) ![屏幕截图 2025-05-02 093247](https://i-blog.csdnimg.cn/img_convert/1fcf3842ed479110537e64533790cd20.png) 这里会有一个问题,就是运行之后,如果我们改我们的所处路径之后,输入pwd,显示的还是我们命令行解释器所处的路径: ![image-20250502093539608](https://i-blog.csdnimg.cn/img_convert/8ec9ae50263fd0f6b1030f75c75e3130.png) 要弄清这个问题,我们先需要彻底理解到底什么是当前路径 **当前路径指的是当前进程的工作目录** ![image-20250502095602190](https://i-blog.csdnimg.cn/img_convert/f506729903fbe1f88779447be69b5593.png) 可以更改工作目录: ![image-20250502095735183](https://i-blog.csdnimg.cn/img_convert/3252dccb09aae9f3322068251ee715fa.png) 如何修改: ![image-20250502095904777](https://i-blog.csdnimg.cn/img_convert/5068d1484b842a2e11e747f24a08a301.png) ![image-20250502095932080](https://i-blog.csdnimg.cn/img_convert/229fd031b83316f01889df586db365ff.png) 所以可以解释为什么我们自己写的shell,cd的时候,路径没有变化 答:是因为在fork()之后,是子进程执行的cd,子进程也有自己的工作目录,cd其实更改的是子进程的目录,而子进程在执行cd完毕就没有了,继续用pwd命令时用的是父进程(即是shell),父进程的工作目录可没有变 那么我们上面的命令行解释器可以在读取输入的命令后加上: ![image-20250502101421278](https://i-blog.csdnimg.cn/img_convert/a3045fafa0ba41dc442cb3b53261c77d.png) 那么就可以成功使用cd命令,pwd指向的是cd之后的路径了 ![image-20250502101513979](https://i-blog.csdnimg.cn/img_convert/54ee312b97e7809aec954020e0c96150.png) **像这种不需要让我们子进程来执行,而是让shell自己来执行的命令叫 ------内建/内置命令** **结尾:** **以上就是我们进程控制专题的相关内容啦,怎么样,是不是内容还是很多的,没关系,慢慢消化就行啦( •̀ ω •́ )**

相关推荐
不会c嘎嘎几秒前
Linux --进度条小程序更新
linux·小程序·apache
西阳未落2 小时前
Linux(9)——进程(控制篇——下)
linux·运维·服务器
水番丘山3 小时前
Linux防火墙firewalld——基础命令与实战
linux
dessler3 小时前
Web服务器-一代经典LNMP
linux·运维·nginx
huangyuchi.3 小时前
【Linux】vim编辑器
linux·运维·笔记·编辑器·vim
2401_858286113 小时前
OS9.【Linux】基本权限(下)
linux·运维·服务器
炎码工坊4 小时前
在Linux上安装Docker并配置镜像加速器:从入门到实战
linux·docker·云原生
husertuo4 小时前
Shell基础命令
linux·华为·云计算
huangyuchi.5 小时前
【Linux】权限相关指令
linux·运维·服务器·笔记·指令·权限
☆凡尘清心☆5 小时前
CentOS 7 环境下部署 LAMP
linux·运维·centos