【Linux】进程控制深度了解


风晓
风晓 2024-01-09 13:46:02 64271 赞同 0 反对 0
分类: 资源
🌟前言         最早的时候我们学习了进程的状态,进程优先级和进程切换,当时不把进程控制加在里面,这里我们单独把它拉出来讲解,学习完本章对进程的板块算是熟练掌握了,咱们话不多说,直接进入今天的主题:【Linux】进程控制深度了解。
 
 
 
⭐主体
我们从以下学习【Linux】进程控制深度了解🥰🥰。
 
 
 
🌙进程创建
💫初识fork函数
fork函数的作用:是从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。
fork函数的返回值:在子进程中返回 0 ,父进程中返回子进程pid,子进程创建失败返回 1 。
#include <unistd.h>
pid_t id = fork(void);
在进程调用fork函数时,当控制转移到内核中的fork代码后,内核做:
 
分配新的内存块和内核数据结构给子进程。
将父进程部分数据结构内容拷贝至子进程。
添加子进程到系统进程列表当中。
fork返回,开始调度器调度。
举个栗子:
 
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
 
int main(void)
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ((pid = fork()) == -1)
{
perror("fork()");
exit(1);
}
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
 
运行结果:
 
 
 
可以看到fork前只输出了一次,fork之后输出了两次。
 
过程分析:
 
Before是由父进程打印的。
调用fork函数之后打印的两个After,则分别由父进程和子进程两个进程执行。
fork之前父进程独立执行,而fork之后父子两个执行流分别执行。
注意: fork之后,父进程和子进程谁先执行完全由调度器决定。
 
💫fork函数返回值
子进程返回0。
父进程返回的是子进程的pid。
fork函数为什么给子进程返回0,给父进程返回的是子进程的PID?
 
        一个父进程可以创建多个子进程,而一个子进程只能有一个父进程,因此对于子进程来说父进程是不需要被标识的,而对于父进程来说,子进程是需要标识的,因为父进程创建子进程的目的是让其指向对于的任务,只有直到了各个子进程的PID,才能更有效率的工作。
 
为什么fork函数有两个返回值?
 
        父进程调用fork函数后,为了创建子进程,fork函数内部会进行一系列复杂的操作,包括创建子进程PCB,虚拟地址空间,创建子进程对应的页表等等。子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表中。
 
 
 
        在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因。
 
💫写时拷贝
当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的。
此时父子进程的代码和数据通过页表映射到物理内存的同一块空间。
当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,再进行修改。
 
 
1、为什么数据要进行写时拷贝?
 
多进程运行需要独享各种资源,多进程运行期间是互不干扰,不能让子进程的修改影响到父进程。
 
2、为什么不在创建子进程的时候就进行数据的拷贝?
 
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。
 
3、代码会不会进行写时拷贝?
 
90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。
 
问题1. 地址空间不隔离
所有程序都直接访问物理内存,程序使用的物理空间不是相互隔离的。万一进程越界进行非法操作,这样是非常不安全的。
 
问题2. 内存使用效率低
没有有效的内存管理机制,通常执行一个程序时,监控程序需要将其整个程序装入内存然后开始执行。
 
问题3. 程序运行的地址不确定
因为每次需要装入运行时,我们都需要给他分配一块足够大的物理空间,而这个物理空间是不确定的。
 
💫fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
💫fork调用失败原因
系统中有太多的进程
实际用户的进程数超过了限制
🌙进程终止
💫进程退出场景
代码运行完毕,结果正确。
代码运行完毕,结果不正确。
代码异常终止(进程崩溃)。
💫进程退出码
我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。
既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。
当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。
举个栗子1:
 
#include <stdio.h>
 
int main()
{
printf("hello linux\n");
return 0;
}
使用指令查看进程退出码1:
 
echo $?
 
 
过程分析1:
 
为什么以0表示代码执行成功,以非0表示代码执行错误?
 
因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。
 
C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:
 
举个栗子2:
 
#include <stdio.h>
#include <string.h>
 
int main()
{
int i = 0;
for(i = 0;i < 100;i++)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
运行结果2:
 
 
 
过程分析2:
 
        实际上Linux中的ls、pwd等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。可以看到,这些命令成功执行后,其退出码也是0。
 
 
 
💫进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
 
从main返回,return退出
调用exit
_exit
异常退出:
 
ctrl+c,信号终止
1.return
return是一种常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数,会将main的返回值当做 exit的参数。
 
 
 
2.exit
使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:
 
执行用户通过atexit或on_exit定义的清理函数。
关闭所有打开的流,所有的缓存数据均被写入。
调用_exit函数终止进程。
举个栗子:
 
#include <stdio.h>
#include <stdlib.h>
 
void show()
{
printf("hello linux\n");
exit(1);
}
 
int main()
{
show();
return 0;
}
运行结果:
 
 
 
结果分析:
 
exit终止进程前会将缓冲区当中的数据输出。
 
3._exit函数
使用_exit函数退出进程的方法我们并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。
 
举个栗子:
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
void show()
{
printf("hello linux");
_exit(2);
}
 
int main()
{
show();
return 0;
}
运行结果:
 
结果分析:
 
使用_exit终止进程,则缓冲区当中的数据将不会被输出。
 
 
 
💫进程异常退出
情况一:向进程发生信号导致进程异常退出。
 
例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
 
情况二:代码错误导致进程运行时异常退出。
 
例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。
 
🌙进程等待
💫进程等待的必要性
子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
💫获取子进程状态status
        下面会介绍有关进程等待的两个函数wait和waitpid,它们都有一个status参数,改参数是一个输出型参数,由操作系统进行填充。如果对该参数传入NULL,表示不关心子进程的退出状态信息。反之,操作系统会通过该参数,将子进程的退出状态信息反馈给父进程。
 
如何理解参数status?
 
        status实际上是一个整型变量,但是status不能当作整型里看待,因为status的不同的比特位所代表的信息不同(只研究低16位比特位)。在status的低16位比特位中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,低7位表示终止信号,而第8位是core dump标志。需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。
 
 
 
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
 
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号
对于此,系统当中提供了两个宏来获取退出码和退出信号。
 
WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码
需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。
 
💫进程等待的方法
1.wait方法
函数原型:pid_t wait(int* status);
作用:等待任意子进程。
返回值:等待成功返回被等待进程的pid,等待失败返回-1。
参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
举个栗子:
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0) 
{
int count = 10;
while (count--) 
{
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
int status = 0;
pid_t ret = wait(&status);
if (ret > 0) 
{
printf("wait child success...\n");
if (WIFEXITED(status)) 
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
sleep(3);
return 0;
}
 
利用指令对进程进行实时监控:
 
while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
运行结果:
 
 
 
结果分析:
 
创建子进程后,父进程可使用wait函数一直等待子进程,直到子进程退出后读取子进程的退出信息。
这时我们可以看到,当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。
1.waitpid方法
函数原型:pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子进程或任意子进程。
返回值:
等待成功返回被等待进程的pid。
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
参数:
等待成功返回被等待进程的pid。
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
举个栗子:
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child          
int count = 10;
while (count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father           
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret >= 0){
//wait success                    
printf("wait child success...\n");
if (WIFEXITED(status)){
//exit normal                                 
printf("exit code:%d\n", WEXITSTATUS(status));
}
else{
//signal killed                              
printf("killed by siganl %d\n", status & 0x7F);
}
}
sleep(3);
return 0;
}
 
运行结果:
 
 
 
结果分析:
 
创建子进程后,父进程可使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息。
被信号杀死而退出的进程,其退出码将没有意义。
🌙进程程序替换
💫替换原理
        用fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数 以执行另一个程序。当进程调用一种exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动 例程开始执行。调用exec 并不创建新进程 , 所以调用 exec 前后该进程的 id 并未改变。
 
1.创建子进程的目的?
 
执行父进程磁盘代码的一部分;
让子进程加载磁盘上指定的程序到内存中,执行新程序的的代码和数据。
 
2.程序替换的本质
 
将指定程序的代码和数据加载到指定的位置
 
3.进程替换的时候,有没有创建新的进程?
 
        我们知道一个进程被创建出来,OS会给它分配进程PCB,mm_struct,页表等信息,同时会将程序的代码和数据加载到物理内存。要知道进程程序替换之后,该进程的PCB,进程地址空间,页表等信息都不会发生改变,仅仅是把一个新的程序的数据和代码替换了原来进程的代码和数据,只是物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid也没有改变。
 
4.子进程进行进程程序替换后,会影响父进程的代码和数据吗?
 
        子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换(调用exec函数)时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据分离,因此子进程进行进程程序替换后不会影响父进程的代码和数据。
 
💫替换函数
一、int execl(const char *path, const char *arg, ...);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
 
举例:
 
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
 
二、int execlp(const char *file, const char *arg, ...);
第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
 
举例:
 
execlp("ls", "ls", "-a", "-i", "-l", NULL);
 
三、int execle(const char *path, const char *arg, ..., char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。
 
举例:
 
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
 
四、int execv(const char *path, char *const argv[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
 
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
 
execv("/usr/bin/ls", myargv);
 
五、int execvp(const char *file, char *const argv[]);
第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
 
举例:
 
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
 
六、int execve(const char *path, char *const argv[], char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。
 
举例:
 
char* myargv[] = { "mycmd", NULL };
 
char* myenvp[] = { "MYVAL=2021", NULL };
 
execve("./mycmd", myargv, myenvp);
 
💫函数解释
这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
如果调用出错,则返回-1。
所以 exec 函数只有出错的返回值而没有成功的返回值。
💫命名理解
l(list):表示参数采用列表的形式,一一列出。
v(vector):表示参数采用数组的形式。
p(path):表示能自动搜索环境变量PATH,进行程序查找。
e(env):表示可以传入自己设置的环境变量。
 
 
事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。
 
下图为exec系列函数族之间的关系:
 
 
   🌟结束语
       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
银河麒麟桌面操作系统V10SP1-2403-update1版本中,通过“麒麟管家-设备管理-硬件信息-硬盘”查看硬盘类型时,显示的是HDD(机械硬盘),而实际上该笔记本的硬盘类型为SSD  43
统信uos安装mysql的实例参考  32
分享解决宏碁电脑关机时自动重启的方法  32
在银河麒麟高级服务器操作系统V10SP3中,需要将默认shell类型修改为csh。  30
分享如何解决报错:归档 xxx.deb 对成员 control.tar.zst 使用了未知的压缩,放弃操作  29
统信uosboot区分未挂载导致更新备份失败  28
格之格打印机dp3300系列国产系统uos打印机驱动选择  26
以openkylin为例编译安装内核  24
最近下载排行榜
银河麒麟桌面操作系统V10SP1-2403-update1版本中,通过“麒麟管家-设备管理-硬件信息-硬盘”查看硬盘类型时,显示的是HDD(机械硬盘),而实际上该笔记本的硬盘类型为SSD 0
统信uos安装mysql的实例参考 0
分享解决宏碁电脑关机时自动重启的方法 0
在银河麒麟高级服务器操作系统V10SP3中,需要将默认shell类型修改为csh。 0
分享如何解决报错:归档 xxx.deb 对成员 control.tar.zst 使用了未知的压缩,放弃操作 0
统信uosboot区分未挂载导致更新备份失败 0
格之格打印机dp3300系列国产系统uos打印机驱动选择 0
以openkylin为例编译安装内核 0
作者收入月榜
1

prtyaa 收益400.53元

2

zlj141319 收益237.46元

3

哆啦漫漫喵 收益231.42元

4

IT-feng 收益219.81元

5

1843880570 收益214.2元

6

风晓 收益208.24元

7

777 收益173.07元

8

Fhawking 收益106.6元

9

信创来了 收益106.03元

10

克里斯蒂亚诺诺 收益91.08元

请使用微信扫码

添加我为好友,拉您入交流群!

请使用微信扫一扫!