美文网首页
1.3进程的控制

1.3进程的控制

作者: 小鼻子球球小昏昏 | 来源:发表于2019-05-29 12:45 被阅读0次

    引出

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

    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)非阻塞等待任意子进程退出,获取它的退出状态,正常退出打印退出码,因为信号退出,打印对应的信号值

    相关文章

      网友评论

          本文标题:1.3进程的控制

          本文链接:https://www.haomeiwen.com/subject/htoetctx.html