Linux进程控制编程
一、进程控制理论
1.进程的定义:
进程是一个具有一定独立功能的程序的一次运行活动,同时也是资源分配的最小单元;
程序是放到磁盘的可执行文件
进程是指程序执行的实例
进程是动态的,程序使静态的:程序是有序代码的集合;进程是程序的执行,通常进程不可在计算机之间迁移;而程序通常对应着文件、静态和可以复制
进程是暂时的,程序是长久的:进程是一个状态变化的过程,程序可长久保存
进程与程序组成不同:进程的组成包括程序、数据和进程控制块
进程与程序的对应关系:通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。
2.进程的生命周期
创建:每个进程都是由其父进程创建进程可以创建子进程,子进程又可以创建子进程的子进程
运行:多个进程可以同时存在进程间可以通讯
撤销:进程可以被撤销,从而结束一个进程的运行
3.进程的状态
执行状态:进程正在占用cpu
就绪状态:进程已具备一切条件,正在等待分配cpu的处理时间片
等待状态:进程因为等待某个资源而睡眠,进程不能使用CPU,若等待事件发生则可将其唤醒
4.进程ID
进程ID(PID):标识进程的唯一数字
父进程的ID(PPID)
启动进程的用户ID(UID)
5.进程互斥
进程互斥是指当有若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止
6。临界资源
操作系统中将一次只允许一个进程访问的资源称为临界资源
7.临界区
进程中访问临界资源的那段程序代码称为临界区,为实现对临界资源的互斥访问,应保证诸进程互斥的进入各自的临界区
8.进程同步
一组并发进程按一定的顺序执行的过程称为进程间的同步
具有同步关系一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件
9.进程调度
概念:按一定算法,从一组待运行的进程中选出一个来占有CPU运行
调度方式:1)抢占式 2)非抢占式
10.调度算法
先来先服务调度算法
短进程优先调度算法
高优先级先调度算法
时间片轮转法
二、进程创建
1.fork()
子进程与父进程复制父进程的变量
有自己的变量空间
返回的两次分别是:
返回给父进程的是 创建的子进程的ID 可以通过父进程找到子进程
返回给子进程的是 0 这0表示子进程没有下一代子进程了
对于小面的例子来说 两次返回值给pid
子进程ID肯定是正整数 所以pid>0时 就是父进程
pid等于0时 是子进程
三种返回值:-1失败 >=0成功
2.第二种创建子进程vfork()
vfork()
子进程与父进程共享父进程的变量
所以变量不能同时被父进程和子进程使用
那么谁先运行?
子线程先运行 如果这个变量父进程也要用那么父进程需要等待
进程结束有两个函数
exit(0)在头文件<stdlib.h>中 会清空文件缓冲区
_exit(0)在头文件<unistd.h>中 不会清空文件缓冲区 更保险一点 什么都不清空。
第二个是使用的系统调用 放在子进程执行语句的最后 结束子进程
为什么不用return 0
因为 子进程结束使用return 0时 会将变量全部释放 而它用的是父进程的变量 子进程先运行 释放变量后 父进程没有变量用了
3.fork PK vfork
3.exec函数族(有六个函数 掌握一个 其他派生)
exec函数后面就不要再写代码了 写了也不执行 因为被替换掉了 这使得子进程不再执行父进程的代码,去执行exec函数中新的任务
第一个参数 是含路径的要执行的文件名 从第二个参数是我想要在命令行写的命令
这里的a是要执行的程序 而1 2 是为想要在命令行输入的命令
./ 是a程序所在路径 而a程序作用就是算显示在命令行上的命令和个数
4.system函数
system函数是调用 与exec的区别 exec是替换
5.进程等待
作用:处理僵尸进程
参数status一般传NULL 其作用是返回子进程的状态的 当父进程不需要了解子进程状态时 就传NULL
当父进程有很多子进程时候 父进程遇到wait时候 等待的是第一个子进程结束,但是不知道第一个结束的是哪个子进程 这时候status参数就有用了 其返回的就是哪个子进程结束 如果wait出错返回的就是-1
6.第二种进程等待
pid_t waitpid(pid_t pid,int *status,int options)
第一个参数是等待哪个子进程
当去<-1时 意思是取这个值的绝对值 看哪个进程组的ID和这个绝对值相同 等待这个进程组中第一个结束的进程结束
当=-1时 等待当前第一个结束的子进程结束 但需要第三个参数配合
当=0时 等待当前进程组里的第一个结束的子进程结束
当>0就是子进程的ID
第二个参数是
返回结束的子进程的状态
第三个参数
取 0 是阻塞等待
取WNOHANG是非阻塞等待 只是看一眼子进程有没有结束 没结束 也之间返回
返回的是0 表示等待的子进程没有结束
返回的>0 是结束的子进程的ID
返回-1 是没有子进程 或者指定的pid 不存在 或者等待的子进程ID存在 但不是我这个父进程的子进程
7.分析子进程的运行例子代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
void die(const char *msg){
perror(msg);
exit(1);
}
void child2_do(){
printf("in child2:execute 'date'\n");
sleep(5);
if(execlp("date","date",NULL) < 0){
perror("child2 execlp");
}
}
void child1_do(pid_t child2,char *argv){
pid_t pw;
do{
if(*argv == '1'){
pw = waitpid(child2,NULL,0);//阻塞等待child2子进程的结束
}
else{
pw = waitpid(child2,NULL,WNOHANG);//非阻塞等待 看一眼child2子进程是否结束
//没结束父进程继续运行 waitpid返回给pw的值是0
}
if(pw == 0){
printf("in child1 process:\nThe child2 process has not exited!\n");
sleep(1);
}
}while (pw == 0);
if(pw == child2){
printf("get child2 id = %d\n",pw);
sleep(5);
if(execlp("pwd","pwd",NULL) < 0){
perror("child1 execlp");
}
}
else{
printf("erreor occured!\n");
}
}
void father_do(pid_t child1,char *argv){
pid_t pw;
do{
if(*argv == '1'){
pw = waitpid(child1,NULL,0);
}
else{
pw = waitpid(child1,NULL,WNOHANG);
}
if(pw == 0){
printf("in father process:\nThe child1 process has not exited!\n");
sleep(1);
}
}while(pw == 0);
if(pw == child1){
printf("get child1 id = %d\n",pw);
if(execlp("ls","ls","-l",NULL) < 0){
perror("father execlp\n");
}
}
else{
printf("error occured\n");
}
}
int main(int argc , char *argv[]){
pid_t child1,child2;
if(argc < 3){
printf("input error! Usage waitpid [0 1] [0 1]\n");
exit(1);
}
child1 = fork();
if(child1 < 0){
die("child1 fork");
}
else if(child1 == 0){
child2 = fork();
if(child2 < 0){
die("child2 fork");
}
else if(child2 == 0){
child2_do();
}
else{
child1_do(child2,argv[1]);
}
}
else{
father_do(child1,argv[2]);
}
return 0;
}
此函数 产生了三个进程 其中child2 孙子进程 执行的是查看系统日期 需要睡眠5s
child1子进程是查看路径 然后睡5s 但是他使用了waitpid 如果给第三个参数传入的是 0 则是阻塞等待 传WNOHANG 是非阻塞等待 具体看上面第二种进程的等待
father进程是查看目录 其要等待child1结束
如果命令行输入的0 0 即阻塞等待 那么输出的是
如果输入 1 1 即非阻塞等待
三、僵尸进程与孤儿进程的产生和处理
1.僵尸进程
首先 我们要知道什么是僵尸进程?
所谓的僵尸进程就是 子进程先于父进程退出后 需要父进程帮其释放ID 但是父进程没有这样做 这时子进程就会成为僵尸进程
使用代码看一下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==0) //子进程
{
printf("child id is %d\n",getpid());
printf("parent id is %d\n",getppid());
}
else //父进程不退出,使子进程成为僵尸进程
{
while(1)
{}
}
exit(0);
}
用ps可以看到子进程后有一个 ,defunct是已死的,僵尸的意思,可以看出这时的子进程已经是一个僵尸进程了。因为子进程已经结束,而其父进程并未释放其ID,所以产生了这个僵尸进程。
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程的数据结构。在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。这个僵尸进程需要它的父进程来为它收尸,如果他的父进程没有处理这个僵尸进程的措施,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。
2.僵尸进程的危害
如果系统中存在大量的僵尸进程 这会占用大量的系统资源 而系统资源是有限的 所以严重时会导致系统崩溃
3.僵尸进程的处理方法
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“defunct”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。所以孤儿进程不会占资源,僵尸进程会占用资源危害系统。我们应当避免僵尸进程的出现。
解决方式如下:
1):一种比较暴力的做法是将其父进程杀死,那么它的子进程,即僵尸进程会变成孤儿进程,由系统来回收。但是这种做法在大多数情况下都是不可取的,如父进程是一个服务器程序,如果为了回收其子进程的资源,而杀死服务器程序,那么将导致整个服务器崩溃,得不偿失。显然这种回收进程的方式是不可取的,但其也有一定的存在意义。
2):SIGCHLD信号处理
我们都知道wait函数是用来处理僵尸进程的,但是进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,(当然如果不存在子进程 那么wait函数会返回-1 而wait唯一会出错返回-1 只有不存在子进程这种情况)如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
由于调用wait之后,就必须阻塞,直到有子进程结束,所以,这样来说是非常不高效的,我们的父进程难道要一直等待你子进程完成,最后才能执行自己的代码吗?难道就不能我父进程执行自己的代码,你子进程什么时候完成我就什么时候去处理你,不用一直等你?当然是有这种方式了。
实际上当子进程终止时,内核就会向它的父进程发送一个SIGCHLD信号,父进程可以选择忽略该信号,也可以提供一个接收到信号以后的处理函数。对于这种信号的系统默认动作是忽略它。我们不希望有过多的僵尸进程产生,所以当父进程接收到SIGCHLD信号后就应该调用 wait 或 waitpid 函数对子进程进行善后处理,释放子进程占用的资源。
下面是一个处理僵尸进程的简单的例子:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
void deal_child(int num)
{
printf("deal_child into\n");
wait(NULL);
}
int main()
{
signal(SIGCHLD,deal_child);
pid_t pid=fork();
int i;
if(pid==0)
{
printf("child is running\n");
sleep(2);
printf("child will end\n");
}
else
{
sleep(1); //让子进程先执行
printf("parent is running\n");
sleep(10); //一旦被打断就不能再进入睡眠
printf("sleep 10 s over\n");
sleep(5);
printf("sleep 5s over\n");
}
exit(0);
}
进行测试后确定了是在父进程睡眠10s时子进程结束,父进程接收到了SIGCHLD信号,调用了deal_child函数,释放了子进程的PCB后又回到自己本身的代码中执行。我们看看运行结果
我们再来看看signal函数(不是阻塞函数)
signal(参数1,参数2);
参数1:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l查看(共64个)。其实这些信号是系统定义的宏。
参数2:我们处理的方式(是系统默认还是忽略还是捕获)。
eg: signal(SIGINT ,SIG_ING ); //SIG_ING 代表忽略SIGINT信号
eg:signal(SIGINT,SIG_DFL); //SIGINT信号代表由InterruptKey产生,通常是CTRL +C或者是DELETE。发送给所有ForeGroundGroup的进程。 SIG_DFL代表执行系统默认操作,其实对于大多数信号的系统默认动作是终止该进程。这与不写此处理函数是一样的。
我们也可以给参数2传递一个信号处理函数的地址,但是这个信号处理函数需要其返回值为void,并且默认自带一个int类型参数
这个int就是你所传递的第一个信号参数的值(你用kill -l可以查看)
测试一下,如果创建了5个子进程,但是销毁的时候仍然有两个仍是僵尸进程,这又是为什么呢?
这是因为当5个进程同时终止的时候,内核都会向父进程发送SIGCHLD信号,而父进程此时有可能仍然处于信号处理的deal_child函数中,那么在处理完之前,中间接收到的SIGCHLD信号就会丢失,内核并没有使用队列等方式来存储同一种信号
所以为了解决这一问题,我们需要调用waitpid函数来清理子进程。
void deal_child(int sig_no)
{
for (;;) {
if (waitpid(-1, NULL, WNOHANG) == 0)
break;
}
}
这样的话,只有检验没有僵尸进程,他才会返回0,这样就可以确保所有的僵尸进程都被杀死了。具体wait和waitpid之后会发详细的。
4.孤儿进程
所谓的孤儿进程 如其名 就是没有父亲 也就是父进程先于子进程结束
这时候没有父进程帮助子进程释放ID 所以其成为了孤儿进程
孤儿进程会被init进程(也就是ID为1的进程)所接收 帮助其完成后续工作
所以孤儿进程不像僵尸进程一样对系统造成危害 其不占用资源 最终还是被系统所回收
所以一般而言 子进程是先于父进程结束的