网站建设wap站,给公司网站做seo的好处,it外包人员有多惨,网站备案服务内容 作者简介#xff1a;დ旧言~#xff0c;目前大二#xff0c;现在学习Java#xff0c;c#xff0c;c#xff0c;Python等 座右铭#xff1a;松树千年终是朽#xff0c;槿花一日自为荣。 目标#xff1a;熟练掌握Linux下的进程控制 毒鸡汤#xff… 作者简介დ旧言~目前大二现在学习JavaccPython等 座右铭松树千年终是朽槿花一日自为荣。 目标熟练掌握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.hint 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.hint main()
{printf(hello linux\n);return 0;
}使用指令查看进程退出码1
echo $? 过程分析1 为什么以0表示代码执行成功以非0表示代码执行错误 因为代码执行成功只有一种情况成功了就是成功了而代码执行错误却有多种原因例如内存空间不足、非法访问以及栈溢出等等我们就可以用这些非0的数字分别表示代码执行错误的原因。
C语言当中的strerror函数可以通过错误码获取该错误码在C语言当中对应的错误信息
举个栗子2
#include stdio.h
#include string.hint 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
异常退出
ctrlc,信号终止
1.return
return是一种常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数,会将main的返回值当做 exit的参数。 2.exit
使用exit函数退出进程也是我们常用的方法exit函数可以在代码中的任何地方退出进程并且exit函数在退出进程前会做一系列工作
执行用户通过atexit或on_exit定义的清理函数。关闭所有打开的流所有的缓存数据均被写入。调用_exit函数终止进程。
举个栗子
#include stdio.h
#include stdlib.hvoid 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.hvoid show()
{printf(hello linux);_exit(2);
}int main()
{show();return 0;
}运行结果
结果分析
使用_exit终止进程则缓冲区当中的数据将不会被输出。 进程异常退出 情况一向进程发生信号导致进程异常退出。 例如在进程运行过程中向进程发生kill -9信号使得进程异常退出或是使用CtrlC使得进程异常退出等。 情况二代码错误导致进程运行时异常退出。 例如代码当中存在野指针问题使得进程运行时异常退出或是出现除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[] { MYVAL2021, 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[] { MYVAL2021, 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系列函数族之间的关系 结束语 今天内容就到这里啦时间过得很快大家沉下心来好好学习会有一定的收获的大家多多坚持嘻嘻成功路上注定孤独因为坚持的人不多。那请大家举起自己的小手给博主一键三连有你们的支持是我最大的动力回见。