Linux 进程控制

发布于:2025-05-30 ⋅ 阅读:(19) ⋅ 点赞:(0)

目录

前言

fork函数

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

2.进程终止

进程退出的情况

3.进程等待

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

1.创建子进程的目的

2.进程程序替换的原理

3. 其他的exec*函数

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


前言

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

fork函数

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

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

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

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

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

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

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

image-20250529161140133

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,地址是一样的,但是内容却不一样

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

image-20250529160907488

fork常规用法:

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

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

fork调用失败的原因:

1.系统中有太多的进程

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

2.进程终止

./mytest —— 运行一个进程

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

(main -> return ? ;)

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

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

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

退出码:

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

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

1.可以自定义

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

系统给的部分错误码含义

image-20250428141748183

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

进程退出的情况

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

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

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

正常进程如何退出:

a.main函数return返回

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

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

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

都是终止当前进程

image-20250428150714257

3.进程等待

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

进程等待的必要性:

image-20250529161337909

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

进程等待相关头文件

#include<sys/types.h>

#include<sys/wait.h>

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

image-20250428185917084

image-20250428164131616

image-20250428164101661

image-20250428164703210

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

image-20250529161728938

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

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

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

image-20250529161807641

关于waitpid的第二个参数status:

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

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

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

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

image-20250529161948913

[^]  退出状态来表示结果是否正确,终止信号表示是否正常退出 

子进程正常退出:

image-20250428172427939

image-20250428172404918

子进程异常退出

让子进程发生除0错误

image-20250428172652284

此时拿到的信号是8号(8号代表浮点数错误,因为除0了)

image-20250428172725912

image-20250428172917980

等待的本质:检测子进程退出信息(在子进程的pcb中),并将子进程退出信息通过status拿回父进程的上下文中

再谈进程退出:

1.进程退出会变成僵尸进程——会把自己的退出结果写入到自己的task_struct中

2.wait/waitpid是一个系统调用,表示操作系统也有资格、能力去读取子进程的task_struct

综上,wait/waitpid是通过操作系统从退出子进程的task_struct中获取退出码(信息)的

阻塞和非阻塞:

image-20250529162058046

非阻塞轮旋等待——父进程一直在询问子进程退出没有

image-20250429182254107

非阻塞有什么好处?

答:不会占用父进程的所有精力,可以在轮询期间干干别的事情

image-20250429190048046

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

1.创建子进程的目的

a.想让子进程执行父进程代码的一部分(执行父进程对应磁盘代码的一部分)

b.想让子进程执行一个全新的程序(让子进程想办法加载到磁盘上指定的程序,执行新程序的代码和数据)—— 进程的程序替换

替换原理:⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变

image-20250529162143738

替换函数:

int execl(const char* path,const char* arg,...); ——将指定的程序加载到内存中,让指定进程执行;第一个参数是用于找到该程序,第二个参数是表示如何执行(你在命令行中怎么执行,就怎么传参),第三个...为可变参数列表,可传多种参数

image-20250429202353983

image-20250429202607321

执行ls

可以让我们的程序去用c把别人的程序调用起来——程序替换

2.进程程序替换的原理

  a.程序替换的本质,就是将指定的代码和数据加载到指定的位置(覆盖自己的代码和数据)

  b.进程替换的时候没有创建新的进程

所以在exec*类函数成功执行后,后面的代码已经被覆盖了,不会执行

image-20250429205458047

exec*类函数成功执行返回不会有返回值(不需要,因为成功执行就代表和接下来的原来的代码无关了,判断返回值没有意义),执行失败会返回-1,只要返回,一定是错误了

image-20250429205932693

多进程时:

image-20250429213527563

image-20250429213601346

image-20250430114933885

[^]  在程序替换时,代码也可能发生写时拷贝 

3. 其他的exec*函数

image-20250529162327148

前面的execl函数中l的意思是list:将参数一个一个的传入exec*

1.

image-20250430120031628

p:path:如何找到程序的功能;带p字符的函数,不需要告诉我程序的路径,只需要告诉我是谁,我会自动在环境变量PATH,进行可执行程序的查找

image-20250430120718731

[^]  两个ls并不重复,第一个ls是告诉系统我要执行谁,第二个ls是告诉系统我想怎么执行 

2.

image-20250430125014586

v:vector:可以将所有的执行参数放入数组中,统一传递,而不用进行使用可变参数方案

image-20250430134824175

前两个结合一下就是3:

image-20250430154500785

可以使用程序替换,调用任意后端语言对应的可执行程序

4.

image-20250430165945194

e:自定义环境变量

image-20250430181819145

是先执行main函数,还是先加载exec*函数?

image-20250430182340785

由于我们需要先把程序加载到内存中,所以理应是exec*函数先加载,main函数的参数数据来源就是从exec *函数中来的

1、2、4结合起来就是5:

image-20250430183814537

上面都是基于系统调用做的封装,为了让我们有更多的选择性

image-20250529162456056

真正的执行程序替换的系统调用接口为:

image-20250430184006625

image-20250529162539989

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

相关源代码可看:myshell 

屏幕截图 2025-05-02 093211

屏幕截图 2025-05-02 093232

屏幕截图 2025-05-02 093247

这里会有一个问题,就是运行之后,如果我们改我们的所处路径之后,输入pwd,显示的还是我们命令行解释器所处的路径:

image-20250502093539608

要弄清这个问题,我们先需要彻底理解到底什么是当前路径

当前路径指的是当前进程的工作目录

image-20250502095602190

可以更改工作目录:

image-20250502095735183

如何修改:

image-20250502095904777

image-20250502095932080

所以可以解释为什么我们自己写的shell,cd的时候,路径没有变化

答:是因为在fork()之后,是子进程执行的cd,子进程也有自己的工作目录,cd其实更改的是子进程的目录,而子进程在执行cd完毕就没有了,继续用pwd命令时用的是父进程(即是shell),父进程的工作目录可没有变

那么我们上面的命令行解释器可以在读取输入的命令后加上:

image-20250502101421278

那么就可以成功使用cd命令,pwd指向的是cd之后的路径了

image-20250502101513979

像这种不需要让我们子进程来执行,而是让shell自己来执行的命令叫 ——内建/内置命令

结尾:

 以上就是我们进程控制专题的相关内容啦,怎么样,是不是内容还是很多的,没关系,慢慢消化就行啦( •̀ ω •́ )


网站公告

今日签到

点亮在社区的每一天
去签到