1.3进程的控制

引出

我们学习了进程,是为了去用多进程,那么,为什么需要用到多进程呢?

1:为了提高效率,支持大用户量的并发。

2:一些大型的服务器程序,是常年补下电的,需要一直运行,而服务器系统崩溃是很严重的事情,那么,就需要有多进程,一个进程挂掉了不能影响另外一个进程的使用,这样,对用户来说,是体会不到的,不影响业务的进行。

举例:银行怎么提高效率?多开几个窗口,每个窗口都在做业务。

举例:之前在华为的网关产品项目中使用多进程的举例。

一:进程的创建

一个现有进程(父进程)可以通过调用fork()函数来创建一个跟现有进程一模一样的新进程(子进程)。

头文件:  #include <unistd.h>

函数原型:pid_t fork(void);pid_t 实际就是int类型。

返回值:  如果创建子进程成功,则返回给父进程的是子进程的id,返回给子进程的是0。

          如果失败,则返回给父进程的是-1,并置errno。

注意:1:子进程创建的过程:会复制父进程的所有资源,包括堆,栈,rodata段,data段,bss段, 缓冲区。但是系统相关信息,代码段共享。

      2:fork之后父,子进程谁先执行是不确定,取决系统的调度算法。

      3:fork之后,父子进程都是从fork下一条语句开始执行。

      4:fork之后,父子进程拥有独立的4G虚拟地址空间。互相不影响。

      5:fork之后,子进程会继承父进程的打开的文件描述符集合,共享文件状态标志位和文件的偏移量。

例如:打开一个文件,在父亲进程中用文件描述符偏移到文件的中间。如果此时创建子进程,则子进程也是也是从文件的中间开始的。

步骤:1:int main(){fork();printf("hello\n");}  hello打印了两遍,说明进程已经创建。

2:查看返回值,并且在父子进程中个分别打印自己的id。

  3:解释下边代码运行结果打印两边hello的原因。(原因就是fork出来的子进程,会完全的复制父进程的所有资源,包括缓冲区)

    printf("hello");

    pid = fork();


===================================================================

僵尸子进程:子进程结束的时候,父进程没有进行收尸操作(父进程还存在),此时占用资源。

孤儿进程:父进程结束了,子进程会变成孤儿进程,会自动被init进程所收养。

===================================================================

我们看到man帮助中,有个copy_on_write这样的字眼,这个是什么东西?我们来拓展介绍一下。

《写时拷贝技术》

<2>vfork

#include <sys/types.h>

#include <unistd.h>

pid_t vfork(void);

功能:创建子进程

参数:无

返回值:成功,对父进程而言。返回子进程的PID好。

              对子进程而言。返回0.

              错误,返回-1。

fork与vfork的区别:

<1>fork函数父子进程谁先运行不确定,由系统调度决定。

  vfork函数子进程先运行,此时父进程会阻塞,子进程会一直运行在父进程的地址空间,直到子进程调用exit结束后才会运行,如果这时子进程修改了某个变量,这将影响到父进程的变量。


<2>fork 函数的正文段共享,其他段被子进程复制。

    vfork函数的子进程直接共享父进程的虚拟地址空间。

<3> 来看一下父子进程对同一文件的操作

先open一个文件,再fork,然后分别再父子进程中写入内容,结果是追加写

先fork,然后分在父子进程中打开同一文件,然后分别写入内容,结果分别写

为什么?下边第一个图是两个独立的进程打开同一个文件,第二个图是先open,再fork之后父子进程对文件共享的关系图。文件表项是在内核空间,进程间共享的。


二:进程的退出

1:相关退出函数

  <1>return  结束一个函数的执行。(当前程序不一定结束。)

<2>void exit(int status)[库函数]

功能:结束一个进程。结束之前会刷新缓冲区。

参数:@status    进程状态的标志。0表示正常结束,其他表示异常结束。

<3>void  _exit(int status) [系统调用]

功能:结束一个进程。结束之前不会刷新缓冲区。

参数:@status

2:return和exit的区别

2.1. return返回函数值,是关键字;exit是一个函数。

2.2. return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。

2.3. return是函数的退出(返回);exit是进程的退出。

2.4. return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。

2.5. return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。

2.6. 非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。

3:exit和_exit的区别

3.1:exit是库函数,_exit是系统调用,exit是基于_exit的实现。

3.2:exit退出进程会清理IO缓冲区,_eixt不会。

三:进程的替换(exec函数族)

1.环境变量

或者称为全局变量,存在与所有的shell 中,在你登陆系统的时候就已经有了相应的系统定义的环境变量了。Linux 的环境变量具有继承性,即子shell 会继承父shell 的环境变量。

查看当前系统的全部环境变量信息:env命令

修改当前系统环境变量信息:直接修改对应的环境变量的值(临时的,只再当前shell生效)

                          永久修改:修改配置文件(按照层级)   

/etc/profile.d-> /etc/bashrc ->~/.bash_profile -> ~/.bashrc 

~/.bash_profile  用户登录时被读取,其中包含的命令被执行。

    ~/.bashrc  启动新的shell时被读取,并执行。

需要重点关注的:PATH:决定了shell将到哪些目录中寻找命令或程序

举例:a.out的执行,不带./看是否可以?

      修改PATH环境变量,把a.out的文件所在路径加进去,然后再次执行。

2. exec函数族

 <1>头文件

#include <unistd.h>

<2>函数原型

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[]);

int execve(const char *path, char *const argv[], char *const envp[]);

返回值:成功返回0,失败返回-1。

参数:@param path: 可执行文件的路径名

  @param file: 可执行文件名,只能搜索环境变量 PATH 指定的路径

  @parma arg: 可执行文件名以及参数,参数列表需要以 NULL 结尾

  @param argv[]: 参数数组,可以取代参数列表

  @param evnp[]: 环境变量数组

 应用举例:     

execl("/bin/ls","ls","-l",NULL);  ./a.out  argv[0]:./a.out  1 + 2  argv[1]

execlp("ls","ls","-l",NULL);

char * const envp[] = {"USER=root","PATH=/bin",NULL};

execle("./app","./app","1",NULL,envp) ;

char * const argv[] = {"ls","-l",NULL};

execv("/bin/ls",argv);

char * const argv[] = {"ls","-l",NULL};

execvp("ls",argv) ;

char * const argv[] = {"ls","-l",NULL};

char * const envp[] = {"USER=root","PATH=/bin",NULL};

execvpe("ls",argv,envp);

.* 练习

1.自己写一个calc.c,要求实现加减乘除功能。gcc calc.c -o calc

./calc 12 + 20

2.自己写一个execl_home.c,要求使用execl调用calc打印相应的内容

参考代码:calc.c,main.c 

calc.c:./calc 10 - 20    10

execl_home.c:./execl 10 + 20

.*练习

实现一个简单的shell。

问题描述参考《myshell的实现》   

strtok函数的使用。

四:进程的等待

僵尸进程:子进程结束的时候,父进程没有进行收尸操作(父进程还存在),此时子进程还占用资源。这时候的子进程就是僵尸进程。

父进程回收子进程资源的时机:(1)父进程结束  (2)处理子进程结束时候发送的SIGCHILD信号来回收资源。

僵尸进程的危害:僵尸态子进程已经结束,它占用大部分资源已经释放,但是仍然保留PID资源。 如果父进程一直不退出,就一直不会回收子进程的僵尸资源,这样产生僵尸态子进程过多,会导致PID资源耗尽,创建子进程失败。

那为什么还要设计僵尸进程呢?给进程设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时间获取。这些信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间,内存使用量等等)。

解决办法:为了防止产生僵尸进程,在fork子进程之后我们都要wait它们;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。

<1>wait的用法

函数原型:pid_t wait(int  *status)

函数功能:回收僵尸态子进程,如果没有僵尸态的子进程则阻塞,如果没有子进程会立即返回。

函数参数:@status  是一个整型指针,指向的对象用来保存子进程退出时的状态

  a. status若是为NULL ,表示忽略子进程退出时的状态。

  b. status若是不为NULL ,表示保存子进程退出时的状态。

返回值:成功返回僵尸态子进程的PID,失败返回-1(没有子进程)。

大部分的时候,我们不需要关注子进程退出时候的状态,只是想把这个僵尸子进程干掉,这个时候,我们就不需要传递实际的status获取状态,直接传NULL进去就可以了,但是,也有的时候,我们是需要关注这个状态的,比如我确实需要知道子进程是不是正常退出的,还是异常退出的。那我们呢就需要知道传出来的status不同的值代表什么意思。

WIFEXITED(status)    宏返回真表示进程正常退出

WEXITSTATUS(status)  取得子进程exit()返回的结束代码,一般会先用WIFEXITED来判断是否正常结束,然后才使用此宏。

===============================================

WIFSIGNALED(status)  如果子进程是因为信号而结束则,返回值为非0 。

                    否则,返回值为0。

WTERMSIG(status)    取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED来判断后,然后才使用此宏。

注意:wait函数是以阻塞(暂停)方式等待子进程结束,等待当前父进程的任一子进程的退出!

      如果是多个子进程,要实现对所有子进程的收尸操作,需要循环调用wait来实现!

思考:什么是阻塞,什么是非阻塞呢?

阻塞: 得到调用的结果之前。一直等待。直到获得了结果再去做其他的事情。

非阻塞:得到调用的结果之前。你可以做其它的事情。当获得了结果告诉我一声就可以了。

例如:exit(5) 结束子进程。则我们调用WEXITSTATUS(status)就返回5.

练习:

fork一个子进程,子进程打印自己的pid,然后死循环,(用信号终止子进程)

父进程wait子进程结束,要获得子进程终止的信号编号。

<2>waitpid的用法

waitpid 函数常见用法如下:

1. 使用非阻塞的方式等待特定子进程退出

while(waitpid(pid,&status,WNOHANG) == 0)

usleep(50000);

2. 阻塞等待任意子进程退出

waitpid(-1,&status,0);====wait(&status)

waitpid(-1,NULL,0);=====wait(NULL);

3. 非阻塞等待任意子进程退出

waitpid(-1,&status,WNOHANG);

4.阻塞等待特定子进程的退出

waitpid(pid,&status, 0);

.*练习

父  子

使用waitpid(pid, NULL,0)指定这个子进程,阻塞式的等待它退出

waitpid(-1, &sta, WNOHANG)非阻塞等待任意子进程退出,获取它的退出状态,正常退出打印退出码,因为信号退出,打印对应的信号值

推荐阅读更多精彩内容