Linux进程控制

进程创建

以fork()为例,从已存在的进程中创建一个新进程,新进程为子进程,原进程为父进程。

#include<unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
  • 进程调用fork,当控制转移到内核中的fork代码后,内核做:
  1. 分配新内存块和内核数据结构给子进程。
  2. 将父进程部分数据结构内容拷贝到子进程中。
  3. 添加子进程到系统进程列表中。
  4. fork返回,开始调度器调度。

进程 = 内核数据结构(OS维护的) +进程代码和数据(一般是磁盘给的)。
创建子进程,给子进程分配对应的内核结构,必须子进程独有,以确保进程的独立性。理论上,子进程也要有自己的代码和数据。可一般而言,没有加载数据的过程,即子进程没有自己的代码和数据。所以,子进程只能“使用”父进程的代码和数据。
在这里插入图片描述


代码:都是不可写的,只能读取,所以父子共享没有问题。
数据:可能被修改,所以必须分离。
所以针对数据的安全问题,
可以直接把父进程的数据拷贝一份给子进程用(写时拷贝)。
但是,拷贝给子进程的数据,子进程可能用不到,即便用到了也可能只是浅浅的读一下。
依着编译器编译程序的时候,都扣扣搜搜的,知道节省空间的样子。
在这里插入图片描述
所以,只会将 将来会被父进程或子进程写入的数据拷贝一份。
但一般而言,os也不知道那些空间可能会被写入。且提前拷贝了也不一定会立刻使用。
所以os选择了,写时拷贝技术,来进行父子进程的数据分离。

os为何要选择写时拷贝技术?
1.用的时候再分配,是高效使用内存的一种表现。
2.os无法在代码执行前预知哪些空间会被访问。

fork之后,父子进程代码共享,其共享是after共享,还是所有的都共享?
所有的共享。
1.代码汇编完之后,会有很多行代码,每行代码加载到内存后,都会有各自对应的地址。
2.因为进程随时可能被中断(可能并没有被执行完),下次回来,还必须从之前中断的位置继续运行。
所以就要求cpu必须随时记录当前进程执行的位置,所以,cpu内有对应的寄存器数据(EIP,也称pc指针(point code)、程序计数器)。
3.寄存器在cpu内只有一份,但寄存器内的数据可以有多份。父子进程各自调度,各自会修改EIP,但无伤大雅,因为子进程已经认为自己的EIP起始值就是fork之后的代码。

在这里插入图片描述


进程终止

进程终止时,操作系统的动作:
释放进程申请的相关内核数据结构和对应的数据和代码。
本质就是释放系统资源(内存、cpu资源……)。

进程退出的场景:
1.代码运行完毕,结果正确。
2.代码运行完毕,结果不正确。
3.代码异常之中。


main函数的返回值意义是啥?
返回值是进程退出嘛。
平常main()中的return 0是啥意思?为什么总是0?
0表示sucess。返回非0,就意味着运行的结果不正确。
在这里插入图片描述
ehco $?:用来获取最近一个进程,执行完毕的退出码。
echo $?本身也是一个进程,所以第二个ehco $获取的是第一个ehco $的进程退出码。第三个echo $?也同理。所以第二、三个退出码是0.

以求1到100的和为例,通过查询进程退出码检测进程是否运行正确
在这里插入图片描述
同时,非0值有很多个,不同的非0值,可以表示不同给的错误原因。
这样子,程序运行结束后,结果不正确时,方便定位错误的原因细节。
如下图,打印了前十个退出码:
在这里插入图片描述
这些退出码是系统提供给我们使用的。当然,也可以自己定义退出码使用。


进程常见的退出方式:exit

#include<stdlib.h>
void exit(int status);
终止普通进程

如下图,exit与return不同。
return在普通函数通常代表调用结束,在main函数中代表进程退出。
exit在代码的任何地方调用,都表示直接终止进程。
且return是语句,exit是函数。
在这里插入图片描述


进程常见的退出方式:_exit

#include<unistd.h>
void _exit(int status);

printf中没有\n时,数据没有被立即刷新出来,而是存在于相关的缓冲区。exit终止程序时,会将缓冲区的数据刷新出来再终止。

而_exit终止程序时,不会将缓冲区的数据刷新出来再终止。 exit会调用_exit,但在调用exit之前,还做了其他工作: 1.执行用户通过atexit或者on_exit定义的清理函数。 2.关闭所有打开的流,所有的缓存数据均被写入 再后,调用exit。

在这里插入图片描述


进程等待

若子进程退出,父进程不管不顾,可能会造成僵尸进程问题,进而导致内存泄漏问题。
且进程一旦变成僵尸状态,那么kill -9也无法杀死这个进程。
同时父进程是需要知道子进程的运行状况的。
所以父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
在这里插入图片描述
如上图,子进程

进程等待方法:wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
返回值:成功返回被等待进程id,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心的话可以设置成NULL

使用wait方法让父进程阻塞等待:
在这里插入图片描述
父进程执行完毕后,回收子进程资源:
5s~7s这个时间段,子进程变成了僵尸进程。待7s后,父进程执行完毕后,回收子进程。
在这里插入图片描述


进程等待方法:waitpid方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int*status, int options);
返回值:
	(1)当正常返回的时候,waitpid返回收集到的子进程id。
	(2)如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0(3)如果调用中出错,则返回-1,这时error会被设置成相应的值以表示错误所在。
参数:
	pid:
		pid=-1,等待任一个子进程,与wait等效。
		pid>0,等待进程ID与pid相等的子进程
	status:
		WIFEXITED(status):若为正常终止子进程范返回的状态则为真。(查看进程是否正常退出)
		WEXITSTATUS(stauts):若WIFEXITED非零,提取子进程退出码。(查看进程退出码)
	options:
		默认为0,表示阻塞等待。

在这里插入图片描述


获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status可以当做位图来看待
status并不是按照整数来整体使用的。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
父进程通过wait/waitpid函数可以拿到子进程的退出结果,可以用一个全局变量代替wait/waitpid函数嘛?
进程具有独立性,数据会发生写时拷贝后,父进程无法拿到。且就算可以,信号的代替方案也有待商榷。
进程具有独立性,进程退出码、收到的信号,也是子进程的数据,父进程怎么拿到的?
一个进程死亡之后,其数据、资源可以释放了,但是至少要保留其PCB信息。而twait/waitpid函数的工作本质其实是,读取task_struct结构中保留的进程退出时的退出结果信息。
在这里插入图片描述

在linux内核中,task_struct中存在进程的退出结果和接受的信号

task_struct是内核数据对象,wait/waitpid函数有资格调用嘛?
有。wait/waitpid算系统调用。

使用系统提供的宏,获取退出码
在这里插入图片描述


waitpid方法的参数options

options默认为0,代表阻塞等待。
WNOHANG选项,代表父进程非阻塞等待。
WNOHANG是一个被系统定义的宏值。
在这里插入图片描述

非阻塞等待:
父进程通过waitpid来进行等待,如果子进程没有退出,立马返回waitpid这个系统调用。

属于操作系统的内核中的waitpid实现的伪代码:
检测子系统退出状态,查看task_struct中子进程的运行信息
waitpid(child_id, status, options)
{
   
	if (status == 退出)
	{
   
		return child_pid;
		//waitpid是父进程调用的,
		//通过 status |= child->sig_number 、 status |= ((child->exit)>>8)
		//父进程可以直接拿到子进程的退出码和受到的信号
	}
	else if (status == 没退出)
	{
   
		if (options == 0)
		{
   
			//为0,默认挂起状态。
			//拿父进程的pcb,father_pcb将其挂入等待队列中。
			//进程阻塞的本质,是进程阻塞在系统函数内部。
			//当运行条件满足时,父进程被唤醒,EIP寄存器中留存的是地址,只想的是是if(options==0)这行代码,所以从这行代码向后继续指向。
		}
		else if (options == WNOHONG)
		{
   
			return 0;//不阻塞进程
		}
		return 0;
	}
	else
	{
   
		//出错、其他原因……
		return -1;
	}
}

···

进程程序替换

· forl的常规用法:

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

程序替换,是通过特定的接口,加载到磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间中。让子进程执行一个其他 程序。


替换原理
用fork创建子进程后,子进程执行的是和父进程相同的程序(有可能执行不同给的代码分支)。子进程往往要用exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新进程的启动例程开始执行。
调用exec并不创建新进程,所以调用exec函数前后该进程的id并未改变。
程序替换,是通过特定的接口,加载到磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间中。
在这里插入图片描述

进程替换,有没有创建新的子进程?
没有。进程早就存在了,进程替换也只是改变映射关系,并不影响进程PCB的优先状态,自然没有创建新的子进程。

操作系统是如何将程序放入(加载)到内存的?
exec函数,就是如何加载程序到内存的函数。


替换函数

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

#include<unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ... char *const envp[]);
int execv(const char *path, const char *const argv[]);
int execvp(const char *file, char *const argv[]); 
//man手册里只有这六个,但是官方手册有七个。

execl()
在这里插入图片描述

  • 参数:
  1. const char *path:上文有提及到:exec函数,就是如何加载程序到内存的函数。加载程序自然需要其地址,所以这个path是带路径+目标文件名。
  2. …(三个点):可变参数列表,代表可以传入多个不定参数。

为什么不打印最后一句printf?
execl是程序替换,调用该函数成功之后,会将当前进程的所有的代码和数据都进行替换,包括已经执行的和没有执行的。
所以一旦调用成功,后续的所有代码,全部不会执行。

execl()只有当他出错了,才会有返回值:-1。
execl为什么成功调用没有返回值?
进程替换时,execl函数本身也属于要被替换掉的代码。所以execl函数成功后,根本不需要返回值。

在这里插入图片描述

为什么要创建子进程?
为了不影响父进程,让父进程聚焦在读取数据、解析数据,指派进程执行代码的功能。


execv
参数:
char *const argv[]:是一个指针数组。
以“ls -a -l”为例,就是将其拆开,将“ls”、“-a”、“-l”分别装入数组。
正因为是一个指针数组,所以一定要以NULL结尾。。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


execlp
参数:
const char *file:这里只需要给执行程序的名称,系统会自动在环境变量path中进行查找
在这里插入图片描述
在这里插入图片描述


execvp
在这里插入图片描述

在这里插入图片描述


execle
相较于execl,execle的参数多了const char *envp[],这个参数接受的是环境变量,也是一个指针数组。

获取环境变量
#include<stdlib.h>
char *getenv(const char *name);

环境变量具有全局属性,可以被子进程继承下去。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


如何指向自己写的执行程序?
上面的ls例子,是系统提供的。
下面以用exec.c调用自己写的mycmd为例。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


命名规律
综上,不难发现,exec*的功能就是加载器的底层接口。 整理下命名规律

  • l(list):表示参数采用列表
  • v(vector):参数用数组
  • p(path):有p自动搜索环境变量PATH
  • e(env):表示自己维护环境变量
函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 不是
execlp 列表
execle 列表 不是 不是,得自己组装环境变量
execv 数组 不是
execvp 数组
execve 数组 不是 不是,得自己组装环境变量

在这里插入图片描述

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-02-13 00:48:02       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-13 00:48:02       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-13 00:48:02       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-13 00:48:02       18 阅读

热门阅读

  1. MacOS 制作 TF 卡/ U 盘镜像

    2024-02-13 00:48:02       33 阅读
  2. 4.8 Binance_interface APP 币本位合约交易-市价单开仓

    2024-02-13 00:48:02       37 阅读
  3. 【深度学习】S1 预备知识 P1 张量

    2024-02-13 00:48:02       27 阅读
  4. 老兵(10)

    2024-02-13 00:48:02       29 阅读
  5. redis过期淘汰策略、数据过期策略与持久化方式

    2024-02-13 00:48:02       26 阅读
  6. python 对Windows关机/重启/锁屏

    2024-02-13 00:48:02       27 阅读
  7. Swagger2

    2024-02-13 00:48:02       32 阅读
  8. Spring Boot + Lua = 王炸!

    2024-02-13 00:48:02       29 阅读
  9. 【嵌入式开发】70

    2024-02-13 00:48:02       26 阅读
  10. STM32 7-8

    STM32 7-8

    2024-02-13 00:48:02      27 阅读
  11. C++ 同构数,的问题。

    2024-02-13 00:48:02       30 阅读