进程控制

一、进程创建
在进程概念那一章节,我们已经了解到了基本的进程创建的方式,现在我们在对其做一个回顾与总结。
1、fork函数创建子进程
(1)fork的概念
在一个已经存在的进程中创建一个新进程,原进程为父进程,新进程为子进程 。其中,子进程完全以父进程为模板进行创建,将父进程的数据结构拷贝给子进程,对于子进程的数据进行写时拷贝。
现在,我们再来认识一下fork

#include <unistd.h>
pid_t fork(void);
//返回值为pid_t,类似一个整数
//在Linux手册中,对于返回值是这样解释的
On  success,  the PID of the child process is returned in the parent, and 0 is returned in the child.
On failure, -1 is returned in the parent, no child process is created, and  errno  is  set  appropri-ately.
总结起来就是:
成功父进程返回子进程的pid,子进程返回0
失败返回-1.

(2)fork的应用
现在,我们来创建一个子进程

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4
  5 int main()
  6 {
  7     pid_t id;
  8     id=fork();
  9     if(id<0)//unsucessful
10     {
11         perror("fork");
12         return 0;
13     }
14     else if(id==0)//child
15     {
16         while(1)
17         {
18         printf("i am child,pid:%d,ppid:%d\n",getpid(),getppid());
19         sleep(1);
20         }
21     }
22     else//father
23     {
24         while(1)
25         {
26             printf("i am father!,pid:%d,ppid:%d\n",getpid(),getppid());
27             sleep(1);
28         }
29     }
30     return 0;
31 }

这里写图片描述
总结:

* 进程调用fork,当控制转移到内核中的fork代码后,内核做:
* 分配新的内存块和内核数据结构给子进程
* 将父进程部分数据结构内容拷贝至子进程
* 添加子进程到系统进程列表当中
* fork返回,开始调度器调度

具体如下图所示:
这里写图片描述
因此,fork之前,父进程单独运行,fork之后,父子两个进程分流,各自执行。具体谁先运行,完全有调度器决定。
(3)写时拷贝
在这里,由于fork是拷贝父进程的地址空间,因此他拥有独立的地址空间以及页表PCB等,因此,子进程与父进程可以完全执行不同的代码(进程的程序替换),从而替换之前的代码,从而可以提高执行的效率问题。
在上面的代码中定义局部或者全局变量i,且令i=10,在父进程与子进程中改变i的值,则i的值在这两个进程中不一样,如图所示:
这里写图片描述
由上图也可以知道,fork创建子进程,数据结构拷贝(有关替换,在这一节的后面介绍),数据写时拷贝。
对于写时拷贝,总结起来就是下面这样
这里写图片描述
(4)fork的常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,⽗进程等待客户端请求,生成子进程来处理请求。
  • ⼀个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数(进程的程序替换)

2、vfork创建子进程
(1)vfork的概念

  • vfork用于创建一个子进程,而子进程和父进程共享地址空间,fork的子进程具有独立地址空间
  • vfork保证子进程先运行,在它调用exec或(exit)之后父进程才可能被调度运行。
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
//返回值与函数意义同fork函数。

(2)vfork的使用

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 int i=10;
  5 int main()
  6 {
  7     pid_t id;
  8     id=vfork();
  9     if(id<0)//unsucessful
10     {
11        perror("fork");
12        exit(1);
13     }
14     else if(id==0)//child
15     {
16         i=98;
17         printf("i am child,pid:%d,ppid:%d,%d\n",getpid(),getppid(),i);
18         sleep(1);
19         exit(0);
20    }
21    else//father
22    {
23        printf("i am father!,pid:%d,ppid:%d,%d\n",getpid(),getppid(),i);
24    }
25    return 0;
26 }

结果如下所示:
这里写图片描述
(3)vfork的常规用法
vfork用于创建多个线程,因为他与父进程共享地址空间,因此只是每次创建进程的PCB,极大的节省了时间,有关线程的学习,在后面在了解。

二、有关进程终止
1、进程退出情景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
2、进程退出的常见方法(利用echo $?查看程序退出码,这个可以用来查询程序是否正确)
(1)正常退出
* 从main函数中返回
* 调用exit
* 调用_exit
具体使用方法如下所示:
这里写图片描述
调用exit函数

#include <unistd.h>
void _exit(int status);

这里写图片描述
调用_exit函数

#include <unistd.h>
void _exit(int status);

这里写图片描述
从上面我们可以看出,调用exit函数与_exit函数,出现的结果不同,由这些也可以知道,exit在退出之时,强制刷新缓冲区,将里面的内容输出到显示器上,而对于_exit函数,在退出之前,什么都不管,直接退出,不会对缓冲区中的内容做一些处理。
对于这两个函数,有以下的图来表示
这里写图片描述
这里,在给出一个函数strerror

#include <string.h>
char *strerror(int errnum);
//参数为对应的错误码
//返回值:成功返回相应错误码对应的描述信息,失败返回Unknown error nnn

打印出错误信息

  1 #include<stdio.h>
  2 #include<string.h>
  3
  4 int main()
  5 {
  6     int i=1;
  7     for(;i<256;++i)
  8     {
  9         printf("%d,%s\n",i,strerror(i));
10     }
11     return 0;
12 }

这里写图片描述
注意:在使用echo $?之时,他只是离他最近的上一个进程的退出码,如下所示
这里写图片描述
总结:
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
三、进程等待
父进程可以创建多个子进程,等待任意一个子进程
1、进程等待的必要性
* 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
* 另外,进程一旦变成僵尸状态,那就刀枪不⼊入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。(只能在另一个终端下杀死这个进程了)
* 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
* 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
总结一下,进程等待
1)父进程回收子进程的资源
2)父进程得到子进程的返回值,从而确保子进程的工作进展
2、进程等待的方法
(1)wait方法

#include <sys/types.h>
#include <sys/wait.h> 
pid_t wait(int *status);
//参数为子进程的退出信息,一般由操作系统提供,这是一个输出型参数,如果不关心子进程的退出状态,则可以将参数设置为NULL
//返回值成功返回子进程的pid,失败返回-1

由于父进程可以创建多个子进程,因此wait方法是等待任意一个进程
这里,使用一下wait,在使用wait的时候,先不关注子进程的状态,这个一般以操作系统提供

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<unistd.h>
  5 int main()
  6 {
  7     pid_t id;
  8     id=fork();
  9     if(id<0)
10     {
11         perror("fork");
12         exit(1);
13     }
14     else if(id==0)//child
15     {
16         int count=10;
17         while(count--)
18         {
19
20             printf("i am child!!!pid:%d,ppid:%d\n",getpid(),getppid());
21             sleep(1);
22         }
23     }
24     else//father
25     {
26         printf("i am father!!!pid:%d,ppid:%d\n",getpid(),getppid());
27         sleep(15);
28         pid_t ret=wait(NULL);
29         printf("father quit\n");
30         sleep(1);
31         printf("ret:%d\n",ret);
32     }
33     return 0;
34 }

结果如下所示:
这里写图片描述
如果父进程没有等子进程退出,在这里子进程将形成僵尸状态,
(2)waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程,与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦子进程的ID
这块的含义是,waitpid进行的是阻塞的等待,如果设置这个选项,则进行非阻塞的等待。

阻塞式等待:在等待的时候,什么都不干,只是等待子进程的返回
非阻塞式等待:在等待子进程的同时,仍然在干着自己的事。

实现阻塞式等待

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<unistd.h>
  5 int main()
  6 {
  7     pid_t id;
  8     id=fork();
  9     if(id<0)
10     {
11         perror("fork");
12         exit(1);
13     }
14     else if(id==0)//child
15     {
16         int count=10;
17         while(count--)
18         {
19
20             printf("i am child!!!pid:%d,ppid:%d\n",getpid(),getppid());
21             sleep(1);
22         }
23         printf("child quit\n");
24         exit(123);
25     }
26     else//father
27     {
28         printf("i am father!!!pid:%d,ppid:%d\n",getpid(),getppid());
29         sleep(15);
30         int status;
31         pid_t ret=waitpid(id,&status,0);
32         if(ret>0)
33         {
34             printf("sig:%d,exit code:%d\n",status&0x7F,(status>>8)&0x7F);
35         }
36         printf("father quit\n");
37         sleep(1);
38         printf("ret:%d\n",ret);
39     }
40     return 0;
41 }

结果如下所示:
正确的退出结果如下所示:
这里写图片描述
当子进程执行任务一直不退出时,可通过向子进程发送相应的信号,从而终止进程。如下所示
这里写图片描述

进程的非阻塞式等待方式——利用waitpid中的第三个参数实现

 1 #include<stdio.h>
 2 #include<sys/types.h>
 3 #include<sys/wait.h>
 4 #include<unistd.h>
 5 int main()
 6 {
 7     pid_t id;
 8     id=fork();
 9     if(id<0)
10     {
11         perror("fork");
12         exit(1);
13     }
14     else if(id==0)//child
15     {
16         int count=10;
17         while(count--)
18         {
19
20             printf("i am child!!!pid:%d,ppid:%d\n",getpid(),getppid());
21             sleep(1);
22         }
23         printf("child quit\n");
24         exit(123);
25     }
26     else//father
27     {
30         int status;
31         while(1)
32         {
33             pid_t ret=waitpid(id,&status,WNOHANG);
34             if(ret>0)//等待成功
35             {
36                 printf("sig:%d,exit code:%d\n",status&0x7F,(status>>8)&0x7F);
37             }
38             else if(ret==0)//正常
39             {
40                 printf("child is running!i do other thing!!\n);
41             }
42             else
43             {
44                 break;
45             }
46             sleep(1);
47
48         }
49         printf("father quit\n");
50         sleep(1);
51     }
52     return 0;
53 }

结果如下所示:
这里写图片描述
对于非阻塞的方式,是基于轮询的方式实现的,从而可以让其更加高效的工作

3、获取子进程的status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位,一般为32位):
这里写图片描述
四、进程的程序替换
1、替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另⼀一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
2、替换函数

#include <unistd.h>
 extern char **environ;
 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[]);
//成功返回信息,失败返回-1

3、使用如下所示
利用execl
这里写图片描述
利用execlp
这里写图片描述
利用execv
这里写图片描述
其他函数的利用同上面
现在再来使用一下execle函数—-此函数可以将自己写的环境变量加载进来,从而有利于自己的替换。具体程序如下所示:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 int main()
  5 {
  6     char *const argv[]={"ls","-l","-a","-i",NULL};
  7     char *const envp[]={"PATH=/bin:/usr/bin","TEMR=console",NULL};
  8     pid_t id;
  9     id=fork();
10
11     if(id<0)//unsucessful
12     {
13        perror("fork");
14        return 0;
15     }
16     else if(id==0)//child
17     {
18         //execl("/bin/ls","ls","-l","-a","-i",NULL);
19         //execlp("ls","ls","-l","-a","-i",NULL);
20         //execv("/bin/ls",argv);
21         execle("ps","ps","-ef",NULL,envp);
22    }
23    else//father
24    {
25    }
26    return 0;
27 }
int execve(const char *path, char *const argv[], char *const envp[]);
//事实上,只有execve是真正的系统调⽤用,其它五个函数最终都调⽤用 execve,所以execve在man⼿手册 第2节,其它函数在man⼿手册第3节。

总结:
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采⽤用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

有关进程控制的知识就这么多了,希望大家监督!!!

只有不停的奔跑,才能不停留在原地!!!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值