> 作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等
> 座右铭:松树千年终是朽,槿花一日自为荣。
> 目标:熟练掌握Linux下的进程控制
> 毒鸡汤:在等待的日子里,刻苦读书,谦卑做人,养得深根,日后才能枝叶茂盛。
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
🌟前言
最早的时候我们学习了进程的状态,进程优先级和进程切换,当时不把进程控制加在里面,这里我们单独把它拉出来讲解,学习完本章对进程的板块算是熟练掌握了,咱们话不多说,直接进入今天的主题:【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的情况使得进程运行时异常退出等。
【Linux】进程控制深度了解(下) https://developer.aliyun.com/article/1565492