Linux入门:进程控制
Posted 世_生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux入门:进程控制相关的知识,希望对你有一定的参考价值。
进程控制
1️⃣进程的创建
fork函数初识
在上篇文章中已经讲了fork函数,主要有:
- 创建子进程
- 有两个返回值
- 创建子进程成功返回0,失败返回-1
- 父进程返回子进程的pid
下面重点讲fork的两个返回值。
fork函数的返回值
先简单理解一下返回值0和返回值pid。
返回值0主要表示子进程的创建成功,而返回子进程的pid给父进程就是要让父进程找到这个子进程。
一个常识:“一个父亲可以有多个孩子,一个孩子只有一个父亲”。
创建子进程主要是父进程要分配任务给子进程执行,一个父进程具有多个子进程,所以,父进程需要通过子进程的pid来给子进程分配不同的任务。
那么如何理解fork函数会有两个返回值呢?
int main()
{
printf("________");
pid_t id=fork();
if(id==0)//子进程
{;}
else if(id>0)//父进程
{;}
else
{;}
return 0;
}
在未调用fork函数之前,只有一个进程,就是父进程。调用 fork函数过程中 return 前,子进程就创建完成了,而不是fork执行完再创建完的。
pid_t fork()
{
//拷贝父进程,形成子进程对应的数据结构
stack_struct
mm_struct
页表
文件
其他信息
//os也要管理子进程,task_struct链接到进程列表中,此时子进程添加到系统的进程列表中
走到这里,子进程创建完成了。
在这里父子进程已经开始分别执行了。
所以return 执行两次,分别被子进程和父进程。子进程执行完放回0,父进程执行完返回子进程的pid。
return pid;
}
写时拷贝
一个父进程创建子进程,实则是拷贝了一份父进程数据和代码给子进程,父子代码共享。
在没有对父子进程进行写入时,父子进程的进程地址空间通过页表映射到同一块物理内存中。
当任意一方要进行写入时,os会再开辟一块内存空间给要写入的数据,这种写入的方式称之为写时拷贝。
那么为什么要有写时拷贝呢?
- 进程具有独立性
- 节省空间,子进程不一定会全部使用父进程的数据,而写入的本质是需要的时候进行写时拷贝,按需分配,节省空间
- 延时分配,子进程要使用父进程的数据不一定现在要用,等要用的时候再分配,可以更高效的使用任何的空间
数据会写时拷贝,那么代码会不会写时拷贝?
大多数情况下不会,但是在进程的替换中会有的。
fork函数常规用法
-
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
-
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork函数失败的原因
- 系统中有太多的进程,系统资源不够
- 实际用户的进程数超过了限制
2️⃣进程的终止
我们写C语言代码时都有一个main函数,main函数是程序的入口。而在main函数结尾中都会写return 0,这个return 0到底是什么呢?return 0返回值给了谁呢?
第一个问题:return 0是返回退出码。
第二个问题:main是函数,是函数就会被人调用,其本质是操作系统调用的,return 0返回给系统。
为什么main函数有返回值?
一个程序的运行起来变成进程,来完成某种工作,一个工作的完成和未完成我们是不知道的,而main函数通过返回退出码给系统,系统来翻译退出码的意思来告诉我们是否完成工作。
echo $?//显示最近一次进程退出时的退出码
退出码是0:表示进程跑完成了,结果正确。
退出码!0:进程失败(有很多种原因)
比如我 ls 一下,然后查看ls进程的退出码。
比如我 ls 一个不存在的文件
通过下面代码来查看退出码代表的意思:
#include<stdio.h>
#include<string.h>
int main()
{
int i;
for( i=0;i<150;i++)
{
printf("%d: %s\\n",i, strerror(i));
}
return 0;
}
进程退出的场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
其中代码异常终止的情况,返回的退出码是没有任何意义的。
因为在return前就开始异常……
进程常见退出方法
正常终止(可以通过echo $?
查看最近一次进程退出的退出码)
- 从main函数返回退出的
- 调用exit
- 调用_exit
异常退出:
- CTRL+C,信号终止
进程终止了os做了什么?
释放掉曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据结构中移除。
_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值。
exit函数
#include <unistd.h>
void exit(int status);
exit最后会调用_exit,但在调用之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
这是因为_exit没有刷新缓冲区,没有把缓冲区的数据写入。而exit会刷新缓冲区,会把缓冲区的内容写入。
return退出
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
注意:只有main函数中的return才是进程的退出,其他函数中的return表示函数调用完毕。
3️⃣进程等待
进程等待的必要性
在上一篇博客中,讲到了僵尸进程会造成内存泄漏,但没有说僵尸进程的解决方法,而进程的等待就可以解决僵尸进程。
还有,父进程给子进程派任务完成的如何,我们是知道的。比如:子进程运行完成,结果对还是不对,或者是否正常退出。
必要性:
- 父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。
进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
返回值:
- 成功返回被进程等待的pid,失败返回-1。
参数:
- 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
代码演示:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();//创建子进程
int count=5;
while(1)//利用循环来证明子进程退出,父进程还在的情况下,子进程不会变成僵尸进程
{
if(id==0)
{
while(count > 0){
printf("child: pid(%d),ppid(%d)\\n",getpid(),getppid());
count--;
sleep(1);
}
_exit(11);
}
else{
pid_t id=wait(NULL);//等待子进程退出
printf("father: pid(%d),ppid(%d)\\n",getpid(),getppid());
}
}
return 0;
}
获取子进程的退出信息在waitpid中讲,会了waitpid就会了wait.
waitpid方法
#include<sys/types.h>
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);//有三个参数
参数pid:
- pid=-1,等待任一子进程,与wait等效。
- pid>0,等待其进程ID与pid相等的子进程。就是说waitpid可以选择性等待子进程。
参数status:
- status是进程的退出信息,具体的下面说。
参数options:
- WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
获取子进程status
wait和waitpid,都要一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传NULL,表示不关心子进程的退出信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
但是,status不是简单的当作整形来处理,而是当做位图来看待。
int型status是四个字节,32个比特位,但是退出信息只占用了低16个比特位,其中在低16个比特位中,从低8位开始到16位是来表示子进程的退出状态的,低7位表示终止信号。
看图看真相:
其中当子进程正常终止时,终止信号为0。
当子进程被信号杀死是,进程异常退出,其退出状态没有意义,而终止信号表示其退出的原因。
这样说可能不怎么理解,下面用代码来表示:
- 当子进程正常终止,拿子进程的退出码和退出信号
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
int count=5;
if(id==0)
{
while(count > 0){
printf("child: pid(%d),ppid(%d)\\n",getpid(),getppid());
count--;
sleep(1);
}
_exit(0);
}
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret >= 0)
{
printf("child pid:%d\\n",ret);//打印子进程的pid
printf("status:%d\\n",status);//打印status
printf("child exit code:%d\\n",(status>>8)&0xFF);//打印子进程的退出码
printf("child get signal:%d\\n",status&0x7F);//打印子进程的信号
}
sleep(5);
return 0;
}
- 当子进程异常终止,拿子进程的退出码和退出信号(当异常终止时,子进程的退出码没有任何意义)
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
int count=5;
if(id==0)
{
while(count > 0){
printf("child: pid(%d),ppid(%d)\\n",getpid(),getppid());
count--;
sleep(1);
}
_exit(0);
}
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret >= 0)
{
printf("child pid:%d\\n",ret);//打印子进程的pid
printf("status:%d\\n",status);//打印status
printf("child exit code:%d\\n",(status>>8)&0xFF);//打印子进程的退出码
printf("child get signal:%d\\n",status&0x7F);//打印子进程的信号
}
sleep(5);
return 0;
}
上面获取子进程的退出码和退出信号只是要让你们了解status,当然还有更简单的方式来获取子进程的退出吗和推出信号。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
子进程运行期间,父进程wait的时候做了什么?
在上面代码中,父进程什么都没有做,就是等子进程,这种行为称之为阻塞等待。
什么是阻塞等待呢?
举个例子:小明在寝室楼下叫小王吃饭,小王在楼上说要等一下,之后小明在楼下等小王,在等的这个期间,小明什么事都不做,手机都不能玩,就是两个眼睛一直盯着门口等小王下来,这种方式就叫阻塞等待。
WNOHANG
什么是非阻塞等待?
举个例子:小明在寝室楼下叫小王吃饭,小王在楼上说要等一下,之后小明在楼下等小王,在等的这个期间,小明接着做自己的事情,直到小王下楼和小明一起去吃饭,这种方式就叫非阻塞等待。
在上面获取status的代码中,waitpid的第三个参数(options)设置成立0,这样父进程就会阻塞式等待子进程。
当options设置成WNOHANG,父进程就会非阻塞式等待子进程。
当子进程没有退出时,waitpid返回0。
当子进程退出时,waitpid返回子进程的pid。
当在调用中出错时,返回-1。
4️⃣进程程序替换
到现在我所学的fork函数创建子进程中都是执行父进程的一部分代码。没有把子进程的作用用出来,而进程程序替换就可以把子进程的作用发挥出来。
替换的原理
用fork创建子进程后执行和父进程相同的程序(但有可能执行不同的代码分支),子进程也可以执行其他的程序,这要调用一种exec系列的函数来执行另一个程序。
当进程调用一种exec系列的函数时,该进程的用户空间代码和数据会完全被新程序替换,从新程序的启动例程开始执行。
调用exec不创建新进程,所以调用exec前后该进程的id并不会改变。
返回到上面讲到的写时拷贝,当父子进程公共的数据要被子进程写入时,就会发生写时拷贝,这是针对数据。
当子进程要调用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, char *const argv[]); |
int execvp(const char *file, char *const argv[]); |
int execve(const char *path, char *const argv[], char *const envp[]); |
用execl来演示一下程序替换:
execl函数的参数:
path:是程序的路径
arg:要执行的命令(怎么执行)
…:后续要怎么执行的参数,最后NULL结尾
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0){
printf("child .....\\n");
execl("/usr/bin/ls", "ls","-l","-i","-a",NULL);//替换函数
printf("hello linux\\n");
}
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
printf("father ....\\n");
return 0;
}
execlp函数:
execlp("ls","ls","-l","-i","-a",NULL);//file(名字)
execv函数
char *const mygrav[]={"ls","-l","-i","-a",NULL};
execv("/usr/bin/ls",mygrav);
execvp函数
execlp("ls","ls","-l","-i",mygrav);//file(名字)
execle函数和execl函数没啥区别,就是后面多了个char *const envp[]
就是说execle可以自己设置环境变量。
char *const envp[] = { "MSSS=100000", NULL};
execle("ls", "ls", "-l","-i","-a", NULL, envp);//设置的环境变量会覆盖要执行的程序中的环境变量
看图看代码
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
我们不关心exec函数的返回值。
命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,须自己组装环境变量 |
exexv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是,须自己组装环境变量 |
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在
man手册第3节。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子:
以上是关于Linux入门:进程控制的主要内容,如果未能解决你的问题,请参考以下文章
Linux Supervisor的安装与使用入门---SuSE
LINUX PID 1和SYSTEMD PID 0 是内核的一部分,主要用于内进换页,内核初始化的最后一步就是启动 init 进程。这个进程是系统的第一个进程,PID 为 1,又叫超级进程(代码片段