Linux进程控制

目录

进程创建

fork函数初识

fork函数返回值

写时拷贝

fork常规用法

fork调用失败的原因

进程终止

进程退出场景

进程常见退出方法

_exit函数

exit函数

return退出

进程等待

进程等待必要性

进程等待的方法

wait方法

waitpid方法

获取子进程status

进程程序替换

替换原理

 替换函数

函数解释

做一个简易的shell


进程创建

fork函数初识

在之前进程概念的学习的时候,我们就已经使用到了fork函数,它就是创建子进程的。

下面是其具体的信息:

头文件#include <unistd.h>
使用方法:pid_t fork(void);
返回值:自进程中返回 0 ,父进程返回子进程 id ,出错返回 -1
其中我们也可以通过man手册来查看fork的基本信息:

 那怎么创建子进程的呢?

进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
当一个进程调用 fork 之后, 就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程 。就像之前在说进程地址空间的时候,我们就知道每一个进程是独立的,就是因为其内核数据结构以及代码和数据是独立的。
我们看看代码:

 这是之前写过的代码,之前也解释了为什么两个死循环能同时进行,就是因为两个进程互相独立的执行着各自的代码。

这里看看结果:

fork函数返回值

子进程返回0,
父进程返回的是子进程的pid
你可能会问为什么一个函数会有两个返回值?这是因为在创建子进程的时候会做一系列的工作,而在函数中子进程就已经被创建完成了,所以父子进程都要返回,所以其实是 在函数中两个进程就已经独立运行了。

写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。在之前讲解进程地址空间的时候也提到了写实拷贝,下面通过画图来理解:

fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段 。例如,父进程等待客户端请求,生成子进程来处理请求。(相当于分工,是做事更加高效做同一件事)
一个进程要执行一个不同的程序 。例如子进程从 fork 返回后,调用 exec 函数(分工,高效做不同的事)

fork调用失败的原因

系统中有太多的进程
实际用户的进程数超过了限制

进程终止

在介绍进程终止的之前,先讲讲C/C++中,为什么最后main函数都要return 0呢?

这里的return 0其实是表示程序正常退出的意思。当然也可以返回其他值,只是说返回的这个值对你这个函数的实现用处不是很大罢了,但是main函数返回值是可以在系统中的找到的。这样可以知道你的程序是正常退出还是其他原因退出。那怎么在我们的系统中查看我们执行完的函数的返回值呢?

 我们可以通过echo $?来查看我们最近一次返回的结果

 但是我们再次执行以下刚才的命令,我们就会发现一个现象:

 为什么是0呢?

因为echo其实也是一个程序,我们知道指令是由c语言以及汇编语言写的。所以echo其实也是有返回值的,它返回的就是0;

 在了解了程序退出码之后,我们来了解进程终止

进程退出场景

一共有3种结果:
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止

进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):
1. main 返回
2. 调用 exit
3. _exit
异常退出:
ctrl + c ,信号终止

_exit函数

我们先看看man手册中的介绍:

 我们可以得到一些有用的信息:

头文件:#include <unistd.h>
使用方式:void _exit(int status);
参数: status 定义了进程的终止状态,父进程通过 wait 来获取该值,后面会介绍到wait。
说明: 虽然status是int,但是仅有低8位可以被父进程所用 。所以 _exit(-1) 时,在终端执行 $? 发现返回值是255 。这个后面也会介绍
在介绍_exit之前,我们先看看我们熟悉的exit:

exit函数

头文件:#include <unistd.h>
使用方式:void exit(int status);
那我们这个库里实现的exit和系统接口提供的_exit有什么区别呢?
下面一段代码看出其中的区别:

 我们可以看到这个在休眠3秒之后还是会刷新出缓存区的内容的。

下面是_exit的结果:

可以看到最后结果是系统提供的接口不会帮我们把缓存区的内容刷新出来,但是库里提供的exit是会帮我们把缓冲区的内容刷新出来的。

当然还会做其他工作:

1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
因为exit会调用_exit,我们就可以知道缓冲区是在系统接口之上的,也就是用户级别的缓冲区

return退出

return 是一种更常见的退出进程方法。执行 return n 等同于执行 exit(n), 因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数。

进程等待

进程等待必要性

之前讲过, 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏 。另外,进程一旦变成僵尸状态,那就刀枪不入,“ 杀人不眨眼 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后, 父进程派给子进程的任务完成的如何,我们需要知道 。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

wait方法

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

我们先看看wait的代码使用:

通过脚本可以一直打印我们进程的状态,从而更好的观察:

 

 通过运行结果我们就可以大概知道了这个wait是怎么使用的。而且可以看看wait确实能够很好的解决子进程僵尸状态的问题,从而可以避免内存泄露的问题。

waitpid方法

函数使用方式:pid_ t waitpid(pid_t pid, int *status, int options);

返回值:
当正常返回的时候 waitpid 返回收集到的子进程的进程 ID
如果设置了选项 WNOHANG, 而调用中 waitpid 发现没有已退出的子进程可收集 , 则返回 0
如果调用中出错 , 则返回 -1, 这时 errno 会被设置成相应的值以指示错误所在;
参数:
pid
Pid=-1, 等待任一个子进程。与 wait 等效。
Pid>0. 等待其进程 ID pid 相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真 。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。 (查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
这两种方法的结果:
如果子进程已经退出,调用 wait/waitpid 时, wait/waitpid 会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用 wait/waitpid ,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
在使用waitpid之前我们要了解以下子进程的status:

获取子进程status

wait waitpid ,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待 ,具体细节如下图(只研究 status 16 比特位):

 在了解的status的使用之后,我们就可以看看waitpid的使用:

在使用waitpid之前,我们要知道父进程可以进行阻塞等待,以及非阻塞等待。为什么有这两种结果呢?首先阻塞等待就是父进程全程等待子进程,知道子进程结束,父进程获得子进程的退出信息。而非阻塞等待就是父进程虽然在等待子进程,但是不是全程都等待,而是等待一会,然后再去执行其他的事情。可能你会想那阻塞等待有什么用,不如直接使用非阻塞等待不是更好吗,效率不是更高吗?但事实上,阻塞等待和非阻塞等待都是存在的,这表明了他们各自都着应用场景

阻塞等待:

我们可以让父进程等待子进程,然后在父进程返回的时候查看子进程的信息。

下面是代码实现:

 看看结果:

我们发现父进程正好等到了子进程退出,并获取到了它的退出码。

非阻塞等待:

非阻塞等待就是父进程在子进程执行的时候也同时做的自己的事情

下面我们就在子进程在运行的时候,让父进程做一点其他的事情。

我们可以看到代码的结果:在子进程运行的时候,父进程也在做自己的事情:

进程程序替换

首先我们先回答一个问题:为什么要创建子进程?

我们创建子进程有两个目的:1.帮助父进程执行部分代码,从而提高效率

2.父子进程分别完成不同的工作,从让子进程执行一个全新的代码。

而这里的方法2就是进程程序替换。

替换原理

 替换函数

头文件:#include <unistd.h>

5个替换函数:

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, char *const argv[]);
int execvp(const char *file, char *const argv[]);
记忆方法:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

函数解释

这些函数如果调用成功则加载新的程序从启动代码开始执行 , 不再返回。
如果调用出错则返回 -1
所以 exec函数只有出错的返回值而没有成功的返回值 。( 因为成功了就替换了原来的程序,也就不需要返回值了)
函数的使用方式:

 

 我们可以看到通过使用execlp的接口就可以帮我们完成程序替换,最后输出ls -a -l的结果。

其他几个接口也是大致这样的使用方式

execl:

后面的使用方式大致相同,只是说真正的运用是创建子进程之后,让子进程完成这项工作,这才是进程程序替换的意义。

父子进程分别做着不同的事情: 

事实上 , 只有execve是真正的系统调用 , 其它五个函数最终都调用 execve , 所以 execve man 手册 第 2 节(系统接口部分) , 其它函数在man手册第 3

他们之间的关系: 

做一个简易的shell

首先我们要明确思路:考虑到这个系统和用户的对话:

因此我们需要做到的这5个步骤:

1. 获取命令行
2. 解析命令行
3. 建立一个子进程(fork)
4. 替换子进程(execvp)
5. 父进程等待子进程退出(wait)
我们可以通过画图的形式来确定我们大概了流程:

下面我们进行相关的代码实现:

 看看结果:

但是有一个问题:使用cd命令的时候就有点问题:

为什么会出现这个问题呢?就要说到我们的文件的相关的知识了,后面 我们下一节再来完善这个shell

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值