linux系统编程 进程控制

简介: linux系统编程 进程控制

一、本章讲什么

1. 为什么学习本章

  对于程序员来说,编写程序时有各种语言的区分,但是文字编码形式的程序一旦被转为机器指令被CPU执
行时,对于CPU来说全都是一样的,没有任何区别。
  在Linux上既可以运行JAVA程序,也可以运行C++和c程序,也可以运行任何其他语言所写的程序,所有
这些程序在Linux OS上运行起来,演变为进程时,都相同的启动过程,所以本章虽然是以C程序为例来讲解
的,但是本章所讲的进程控制的API,可以用来加载运行所有语言编写的程序,因此本章内容,特别是进程
控制这部分的内容,有着普适性的意义,所以希望大家能够人很学习本章。

2. 本章内容

本章分成三部分,进程控制、进程关系、守护进程。
(1)进程控制
    本部分内容将会为大家解决如下问题:
  1)有OS的支持时,进程具体是如何启动起来的
      在上一章里面我们简单介绍过,通过exec加载程序后,进程就会启动起来,但是实际过程并
    不是这么简单,所以我们在这一章将会详细介绍程序是如何运行起来,演变为进程的。
      而且这个过程不仅只适合C程序,所有程序都是以这样的过程运行起来的。
      这一章介绍完毕后,大家就能清晰的理解双击快捷图标,或者在命令行执行./a.out后,我
    们的程序到底是怎么运行起来的。
  2)在本章,我们还会讲清楚,有OS支持时,我们的main函数return,或者使用exit、_exit所返回
  的返回值到底给了谁,有什么意义,为什么在平时,这个返回值我们会感觉到并没有什么用处。
  3)不管是java、还是c/c++等语言所写的程序,在内存中运行起来后就演变为了进程,但是由于java
  需要虚拟机来解释执行,中间夹一个虚拟机,使得很多的同学并不能理解java进程是如何产生的,我
  们会对比java和c/c++进程的启动过程,让大家理解java的进程是如何运行起来的。
    如果java程序的运行你理解了,那么与java类似的C#、pyhton等的程序,都是相同的运行过程。
  4)但是学完今天这一章以后,我们的程序可以实现多进程了,不过一般我们写程序都是单进程的,有
  同学有疑问,什么情况下的程序才会涉及到多进程呢?
(2)进程关系
    有OS时,基于OS运行的进程非常多,众多进程之间往往存在着一定的关系,我们在这一部分会介绍
  这些进程之间有些什么关系。
(3)守护进程
    在windows下查看任务管理器时,会看到很多的后台进程,不仅仅是windows,Linux也是一样的
  ,OS启动起来以后,会有很多我们平时看不见的,但是一直在默默运行的后台进程,这些后台进程
  大多其实是守护进程。
    在守护进程这部分,我们将会介绍如何做出一个我们自己的守护进程。

二、有关进程

1. 什么是进程

我们说,将程序代码从硬盘拷贝到内存上,在内存上动态运行的程序就是进程。

2. 多进程并发运行

有OS支持时,会有很多的进程在运行,这些进程都是并发运行的。
什么是并发运行?
  就是CPU轮换的执行,当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复
,由于时间片很短,在宏观上我们会感觉到所有的进程都是在同时运行的,但是在微观上cpu每次只执行某一
个进程的指令。
  当然我们这里说的单核cpu的情况,如果cpu是多核的话,不同的cpu核可以同时独立的执行不同的进程,
这种叫并行运行。所以当cpu是多核时,并发与并行是同时存在的。

3. 进程ID(PID)

什么是PID
  基于OS运行的进程有很多,OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数)
,这个编号就是PID,P就是process——进程的意思。
  这记好比公安局给每个人分配了一个唯一的身份证号(ID)是一样的。
      ps查看:
  如果当前进程结束了,这个PID可以被可以被重复使用,但是所有“活着”的进程,它们的进程ID一定都是
唯一的。
  因为ID的唯一性,当我们想创建一个名字唯一的文件时,往往可以在文件名中加入PID,这样就能保证
文件名的唯一性。
那么PID放在了那里呢?
  进程在运行的过程中,OS会去管理进程,这就涉及到很多的管理信息,OS(Linux)为了管理进程,会为
每一个进程创建一个task_struct结构体变量,里面放了各种的该进程的管理信息,比如第一章介绍的文件
描述符表,又比如我们这里讲的PID。
  所以PID放在了该进程的task_struct结构体变量中,有关task_struct在前面的课程就介绍过,相信
大家不会陌生。
 如何获取PID呢?
  后面回答这个问题。

4. 三个特殊的进程

  OS运行起来后有三个特殊的进程我们需要了解下,他们的PID分别是0、1、2。
  0、1、2这个三个进程,是OS启动起来后会一直默默运行的进程,直到关机OS结束运行,尽管我们总是
忽略它们的存在,但是它们确非常的重要。
  4.1 进程 PID == 0 的进程
  (1)作用
    这个进程被称为调度进程,功能是实现进程间的调度和切换,该进程根据调度算法,该进程会让CPU
  轮换的执行所有的进程。
    怎么实现的?
    当pc指向不同的进程时,cpu就去执行不同的进程,这样就能实现切换。
  (2)这个进程怎么来的
    这个进程就是有OS演变来的,OS启动起来后,最后有一部分代码会持续的运行,这个就是PID==0
  的进程。
    由于这个进程是OS的一部分,凡是由OS代码演变来的进程,都称之为系统进程。
  4.2 进程ID == 1的进程
  (1)作用
    1)作用1:初始化
      这个进程被称为init进程,这个进程的作用是,他会去读取各种各样的系统文件,使用文件
    中的数据来初始化OS的启动,让我们的OS进入多用户状态,也就是让OS支持多用户的登录。
    2)作用2:托管孤儿进程
      什么事孤儿进程,怎么托管的,有关这个问题后面会详细介绍。
    3)作用3:原始父进程
        原始进程————>进程————————>进程————————>终端进程——————>a.out进程
                      |            |             |
                      |            |             |
                      V            V             |
                    进程          进程          进程
                      |            |             |
                      |       |             |
                     ...          ...           ...
  (2)这个进程怎么运行起来的
    这个进程不是OS演变来的,也就是说这个进程的代码不属于OS的代码,这个进程是一个独立的程序
  ,程序代码放在了/sbin/init下,当OS启动起来后,OS回去执行init程序,将它的代码加载到内存,
  这个进程就运行起来了。
  4.3 进程ID == 2的进程
  (1)作用
    页精灵进程,专门负责虚拟内存的请页操作。
    疑问:什么精灵进程?
      精灵进程也叫守护进程,我们后面讲到“守护进程”这一章时,你自然就知道了。
    怎么理解换页操作,我们说当OS支持虚拟内存机制时,加载应用程序到内存时,并不会进行
  完整的代码拷贝,只会拷贝当前要运行的那部分代码,当这部分代码运行完毕后,会再拷贝另一
  部分需要运行的代码到内存中,拷贝时是按照一页一页来操作的,每一页大概4096字节,这就是
  换页操作。
    想了解详细换页操作的同学,请看《计算机体系结构》软件篇4——操作系统部分的课程。
  (2)这个进程怎么运行起来的
      与调度进程一样,也是一个系统进程,代码属于OS的一部分。

5. 获取与进程相关的各种ID的函数

函数原型和所需头文件
    #include <sys/types.h>
    #include <unistd.h>
    pid_t getpid(void);
    pid_t getppid(void);
    uid_t getuid(void);
    gid_t getgid(void);
(1)功能
    1)getpid函数:获取调用该函数进程的进程ID。
    2)getppid函数:获取调用该函数进程的父进程ID,第一个P是parent,第二个process。
    3)getuid函数:获取调用该函数进程的用户ID。
        在什么用户下运行的该进程,得到的就是该用户的用户ID,查看/etc/passed文件,可
      以找到该UID对应的用户名。
    4)getgid函数:获取用户组的ID,也就是调用该函数的那个进程,它的用户所在用户组的组ID。
(2)返回值:返回各种ID值,不会调用失败,永远都是成功的。

三、程序的运行过程

1. 程序如何运行起来的

(1)在内存中划出一片内存空间
(2)将硬盘上可执行文件中的代码(机器指令)拷贝到划出的内存空间中
(3)pc指向第一条指令,cpu取指运行
当有OS时,以上过程肯定都是通过调用相应的API来实现的。
在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec。
fork:开辟出一块内存空间
exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就
运行起来了
    运行起来的进程会与其它的进程切换着并发运行。

2. fork

2.1 函数原型

  #include <unistd.h>
  pid_t fork(void);
  为了便于大家更容易的理解,我们在介绍fork时会适当的隐去一些信息,所以虽然不能保证100%是正确
的,但是我们能够向大家解释清楚fork函数的作用。
(1)函数功能
  从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程。
  复制后有两个结果:
  1)依照父进程内存空间样子,原样复制地开辟出子进程的内存空间
  2)由于子进程的空间是原样复制的父进程空间,因此子进程内存空间中的代码和数据和父进程完全相同
  其实复制父进程的主要目的,就是为了复制出一块内存空间,只不过复制的附带效果是,子进程原样的
拷贝了一份父进程的代码和数据,事实上复制出子进程内存空间的主要目的,其实是为了exec加载新程序的
代码。
(2)函数参数:无参数。
(3)函数返回值
    由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数,当然这个说法有些欠妥,
  但是暂且这么理解。
    1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
    2)子进程的fork,成功返回0,失败返回-1,errno被设置。
(4)代码演示
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
  pid_t ret = 0;
  printf("befor fork\n");
  ret = fork();
  if(ret > 0)
  {
    printf("parent PID = %d\n", getpid());
    printf("parent ret = %d\n", ret);
    sleep(1);
  }
  else if(ret == 0)
  {
    printf("child PID = %d\n", getpid());
    printf("child ret = %d\n", ret);
  }
  printf("after fork\n\n");
  while(1);
  return 0;
}

2.2 说说复制的原理

  Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存
模拟出来的,因此底层的对应的还是物理内存。
  复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会
对应着一片新的物理内存空间,里面放了与父进程一模一样代码和数据。

2.3 父子进程各自会执行哪些代码

(1)父进程
      1)执行fork前的代码
      2)执行fork函数
          父进程执行fork函数时,调用成功会返回值为子进程的PID,进入if(ret > 0){}
        中,执行里面的代码。
          if(ret > 0){}中的代码只有父进程才会执行。
      3)执行fork函数后的代码
  (2)子进程
      1)fork前的代码
          尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行。
      2)子进程调用fork时,返回值为0,注意0不是PID。
        进入if(ret == 0){},执行里面的代码。
        if(ret == 0){}中的代码只有子进程执行。
      3)执行fork后的代码
  (3)验证子进程复制了父进程的代码和数据
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
  pid_t ret = 0;
  //"befor fork"后面如果不加上\n,因为printf是行缓冲原因,所以父进程暂时
  //不会打印(除非遇到printf中\n),只会把数据存放到标准io的缓冲区
  //当中;fork只会子进程会复制父进程的代码和数据的原因导致子进程也会
  //把"befor fork"这个数据存放到子进程标准io的缓冲区当中,所以在子
  //进程打印的时候也会打印出"befor fork".
  //注意:子进程并不会执行fork之前的代码
  printf("befor fork");
  //若加上\n,那么父进程执行完printf之后,标准io的缓冲区就变成空了,
  //同样的,执行fork之后子进程的标准io的缓冲区也会变成空。从而导致
  //子进程不会打印出"befor fork\n"
  //printf("befor fork\n");
  ret = fork();
  if(ret > 0)
  {
    printf("parent PID = %d\n", getpid());
    printf("parent ret = %d\n", ret);
    sleep(1);
  }
  else if(ret == 0)
  {
    printf("child PID = %d\n", getpid());
    printf("child ret = %d\n", ret);
  }
  printf("after fork\n\n");
  while(1);
  return 0;
}

3. 父子进程共享操作文件

3.1 情况1:独立打开文件

多个进程独立打开同一文件实现共享操作,我们在第1章讲过,不过那时涉及到的多个进程是不相干进程
,而现在我们这里要讲的例子,里面所涉及到的两个进程是父子关系,不过情况是一样的。

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
  pid_t ret = 0;
  int fd = 0;
  ret = fork();
  if(ret > 0)
  {
    fd = open("./file.txt", O_RDWR|O_CREAT|O_APPEND, 0664);
    write(fd, "hello\n", 6);
  }
  else if(ret == 0)
  {
    fd = open("./file.txt", O_RDWR|O_CREAT|O_APPEND, 0664);
    write(fd, "world\n", 6);
  }
  return 0;
}

文件表结构

图:

独立打开同一文件时,父子进程各自的文件描述符,指向的是不同的文件表。
  因为拥有不同的文件表,所以他们拥有各自独立的文件读写位置,会出现相互覆盖情况,如果不想相互
覆盖,需要加O_APPEND标志。

3.2 情况2:fork之前打开文件

代码演示

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
  pid_t ret = 0;
  int fd = 0;
  fd = open("./file.txt", O_RDWR|O_CREAT, 0664);
  ret = fork();
  if(ret > 0)
  {
    write(fd, "hello\n", 6);
    printf("p, uid = %d, gid = %d\n", getuid(), getgid());
  }
  else if(ret == 0)
  {
    write(fd, "world\n", 6);
    printf("c, uid = %d, gid = %d\n", getuid(), getgid());
  }
  return 0;
}

文件表结构

图:

  子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的
文件描述符3也会指向这个文件。像这种继承的情况,父子进程这两个相同的“文件描述符”指向的是相同
的“文件表”。
  由于共享的是相同的文件表,所以拥有共同的文件读写位置,不会出现覆盖的情况。
  子进程的0 1 2这三个打开的文件描述符,其实也是从父进程那里继承过来的,并不是子进程自己去
打开的,同样的父进程的0 1 2又是从它的父进程那里继承过来的,最根溯源的话,都是从最原始的进
程哪里继承过来的,我们前面介绍过,最原始的进程是init进程。
  init进程会去打开标准输入,标注输出、标准出错输出这三个文件,然后0 1 2分别指向打开的文件,
之后所有进程的0 1 2,实际上都是从最开始的init进程那里继承而来的。
      init 012   012           012           012       
      原始进程————>进程————————>进程———>...———>终端进程——————>
      012           012
      a.out进程——————>a.out进程
                    |            |             |
                    |            |             |
                    V            V             V
                  进程012     进程012       进程012
                    |            |             |
                    |            |             |
                   ...          ...           ...

4. 子进程会继承父进程的哪些属性

4.1 子进程继承如下性质
  (1)用户ID,用户组ID
  (2)进程组ID(下一篇讲)
  (3)会话期ID(下一篇讲)
  (4)控制终端(下一篇讲)
  (5)当前工作目录
  (6)根目录
  (7)文件创建方式屏蔽字
  (8)环境变量
  (9)打开的文件描述符
      等等
4.2 子进程独立的属性
  (1)进程ID。
  (2)不同的父进程ID。
  (3)父进程设置的锁,子进程不能被继承。
      等等

四、exec加载器

exec加载器就是我们之前介绍的加载函数。

4.1 exec的作用

  父进程fork复制出子进程的内存空间后,子进程内存空间的代码和数据和父进程是相同的,这样没有
太大的意义,我们需要在子进程空间里面运行全新的代码,这样才有意义。
  怎么运行新代码?
  我们可以在if(ret==0){}里面直接写新代码,但是这样子很麻烦,如果新代码有上万行甚至更多的话,
这种做法显然是不行的,因此就有了exec加载器。
  有了exec后,我们可以单独的另写一个程序,将其编译好后,使用exec来加载即可。

4.2 exec函数族

exec的函数有很多个,它们分别是execve、execl、execv、execle、execlp、execvp,都是加载
函数。其中execve是系统函数,其它的execl、execv、execle、execlp、execvp都是基于execve封
装得到的库函数,因此我们这里重点介绍execve函数,这个函数懂了,其它的函数原理是一样的。

4.2.1 execve函数原型

#include <unistd.h>
int execve(const char *filename, char **const argv, char **const envp);
(1)功能:向子进程空间加载新程序代码(编译后的机器指令)。
(2)参数:
    1)filename:新程序(可执行文件)所在的路径名
      可以是任何编译型语言所写的程序,比如可以是c、c++、汇编等,这些语言所写的程序被编译
    为机器指令后,都可以被execve这函数加载执行。正是由于这一点特性,我们才能够在C语言所实
    现的OS上,运行任何一种编译型语言所编写的程序。
      疑问:java可以吗?
        java属于解释性语言,它所写的程序被编译后只是字节码,并不是能被CPU直接执行的
      机器指令,所以不能被execve直接加载执行,而是被虚拟机解释执行。
        execve需要先加载运行java虚拟机程序,然后再由虚拟机程序去将字节码解释为机器
      指令,再有cpu去执行,在后面还会详细讨论这个问题。
    2)argv:传给main函数的参数,比如我可以将命令行参数传过去
    3)envp:环境变量表
(3)返回值:函数调用成功不返回,失败则返回-1,且errno被设置。

4.2.2 代码演示

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
  pid_t ret = 0;
  ret = fork();
  if(ret > 0)
  {
    sleep(1);
  }
  else if(ret == 0)
  {
    extern char **environ;
    //int execve(const char *filename, char **const argv, char **const envp);
    //命令行参数和环境变量可以继承父进程,也可以自定义
    char *my_argv[] = {"fds", "dsfds", NULL};
    char *my_env[] = {"AA=aaaaa", "BB=bbbbb", NULL};
    execve("./new_pro", my_argv, my_env);
  }
  return 0;
}
命令行参数/环境表                    命令行参数/环境表                命令行参数/环境表        
终端窗口进程——————————————————>a.out(父进程)——————————————————————>a.out(子进程)——————————————>新程序
                                                      fork                                    exec
  exec的作用:将新程序代码加载(拷贝)到子进程的内存空间,替换掉原有的与父进程一模一样的代码和
 数据,让子进程空间运行全新的程序。

4.2.3 在命令行执行./a.out,程序是如何运行起来的

(1)窗口进程先fork出子进程空间
(2)调用exec函数加载./a.out程序,并把命令行参数和环境变量表传递给新程序的main函数的形参

4.2.4 双击快捷图标,程序是怎么运行起来的

(1)图形界面进程fork出子进程空间
(2)调用exec函数,加载快捷图标所指向程序的代码
  以图形界面方式运行时,就没有命令行参数了,但是会传递环境变量表。

五、system函数

  如果我们需要创建一个进子进程,让子进程运行另一个程序的话,可以自己fork、execve来实现,但是
这样的操作很麻烦,所以就有了system这个库函数,这函数封装了fork和execve函数,调用时会自动的创
建子进程空间,并把新程序的代码加载到子进程空间中,然后运行起来。
  虽然有system这函数,但是我们还是单独的介绍了fork和execve函数,因为希望通过这两个函数的
介绍,让大家理解当有OS支持时,程序时如何运行起来的。
  system函数原型
   #include <stdlib.h>
   int system(const char *command);
  (1)功能:创建子进程,并加载新程序到子进程空间,运行起来。
  (2)参数:新程序的路径名
  (3)代码演示
        system(“ls”);
        system(“ls -al”);

六、回收进程资源

进程运行终止后,不管进程是正常终止还是异常终止的,必须回收进程所占用的资源。

6.1 为什么要回收进程的资源?

(1)程序代码在内存中动态运行起来后,才有了进程,进程既然结束了,就需要将代码占用的内存空间让
出来(释放)。
(2)OS为了管理进程,为每个进程在内存中开辟了一个task_stuct结构体变量,进程结束了,那么这个结
构体所占用的内存空间也需要被释放。

6.2 由谁来回收进程资源

由父进程来回收,父进程运行结束时,会负责释放子进程资源。

6.3 僵尸进程和孤儿进程

ps查看到的进程状态
R 正在运行
S 处于休眠状态
Z 僵尸进程,进程运行完了,等待被回收资源。

6.3.1 僵尸进程

子进程终止了,但是父进程还活着,父进程在没有回收子进程资源之前,子进程就是僵尸进程。
  为什么子进程会变成僵尸进程?
  子进程已经终止不再运行,但是父进程还在运行,它没有释放子进程占用的资源,所以就变成了占着资源
不拉屎僵尸进程。
  就好比人死后不腐烂,身体占用的资源得不到回收是一样的,像这种情况就是所谓的僵尸。

6.3.2 孤儿进程

没爹没妈的孩子就是孤儿,子进程活着,但是父进程终止了,子进程就是孤儿进程。
  为了能够回收孤进程终止后的资源,孤儿进程会被托管给我们前面介绍的pid==1的init进程,每当被
托管的子进程终止时,init会立即主动回收孤儿进程资源,回收资源的速度很快,所以孤儿进程没有变成僵
尸进程的机会。

七、wait函数

作用:父进程调用这个函数的功能有两个,
(1)主动获取子进程的“进程终止状态”。
(2)主动回收子进程终止后所占用的资源。
  wait函数,在实际开发中用的很少,但是我们这里还是要介绍这个函数,因为如果你理解了这个函数,
你才能理解进程return/exit/_exit所返回的返回值,到底返回给了谁。

7.1 进程的终止

7.1.1 正常终止

(1)main调用return
(2)任意位置调用exit
(3)任意位置调用_exit
  不管哪种方式来正常终止,最终都是通过_exit返回到OS内核的。

7.1.2 异常终止

如果是被某个信号终止的,就是异常终止。
(1)自杀:自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死。
(2)他杀:由别人发一个信号,将其杀死。

7.1.3 进程终止状态

(1)退出状态与“进程终止状态”
  我们在上一章里面将return、exit、_exit的返回值称为“进程终止状态”,严格来说应该叫“退出状
态”,
  return(退出状态)、exit(退出状态)或_exit(退出状态)
  当退出状态被_exit函数交给OS内核,OS对其进行加工之后得到的才是“进程终止状态”,父进程调
用wait函数便可以得到这个“进程终止状态”。
(2)OS是怎么加工的?
  1)正常终止
     进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位
    不管return、exit、_exit返回的返回值有多大,只有低8位有效,所以如果返回值太大,只取低
  8位的值。
  2)异常终止
    进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号
(3)父进程调用wait函数,得到“进程终止状态”有什么用
  父进程得到进程终止状态后,就可以判断子进程终止的原因是什么,如果是正常终止的,可以提取出
返回值,如果是异常终止的,可以提取出异常终止进程的信号编号。
  讲到这里大家就明白了,当有OS支持时,进程return、exit、_exit正常终止时,所返回的返回值
(退出状态),最终通过“进程终止状态”返回给了父进程。
  这有什么用,比如,父进程可以根据子进程的终止状态来判断子进程的终止原因,返回值等等,以决定
是否重新启动子进程,或则做一些其它的操作,不过一般来说,子进程的终止状态对父进程并没有太大
意义。

7.2 父进程如何从内核获取子终止状态

(1)父进程调用wait等子进程结束,如果子进程没有结束的话,父进程调用wait时会一直休眠的等(或者
说阻塞的等)。
(2)子进程终止返回内核,内核构建“进程终止状态”
  如果:
  1)子进程是调用return、exit、_exit正常终止的,将退出状态返回给内核后,内核会通过如下
表达式构建“进程终止状态”
    进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位
  2)子进程是被某个信号异常终止的,内核会使用如下表达式构建“进程终止状态”
    进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号
(3)内核向父进程发送SIGCHLD信号,通知父进程子进程结束了,你可以获取子进程的“进程终止状态”了。
  如果父进程没有调用wait函数的话,会忽略这个信号,表示不关心子进程的“进程终止状态”。
  如果父进程正在调用wait函数等带子进程的“进程终止状态”的话,wait会被SIGCHLD信号唤醒,并获
取进“进程终止状态”
  一般情况下,父进程都不关心子进程的终止状态是什么,所以我们经常看到的情况是,不管子进程返回
什么返回值,其实都无所谓,因为父进程不关心。
  不过如果我们的程序是一个多进程的程序,而且父进程有获取子进程“终止状态”的需求,此时我们就可以
使用wait函数来获取了。

7.2 wait函数原型

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); 
(1)功能:获取子进程的终止状态,主动释放子进程占用的资源
(2)参数:用于存放“进程终止状态”的缓存
(3)返回值:成功返回子进程的PID,失败返回-1,errno被设置。

7.3 从进程终止状态中提取进程终止的原因、返回值或者信号编号

(1)进程状态中所包含的信息
  1)正常终止
     进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位
  2)异常终止
    进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号
(2)如何提取里面的信息
  系统提供了相应的带参宏,使用这个带参宏就可以从“进程终止状态”中提取出我们要的信息。
  提取原理:相应屏蔽字&进程终止状态,屏蔽掉不需要的内容,留下的就是你要的信息。     
  哪里能查到这些带参宏,man查案wait的函数手册,即可看到。
  1)WIFEXITED(status):提取出终止原因,判断是否是正常终止
    (a)如果表达式为真:表示进程是正常终止的
      此时使用WEXITSTATUS(status),就可以从里面提取出return/exit/_exit返回的“
    退出状态”。
    (b)为假:不是正常终止的
  2)WIFSIGNALED(status):提取出终止原因,判断是否是被信号杀死的(异常终止)
    (a)如果表达式为真:是异常终止的
          此时使用WTERMSIG(status),就可以从里面提取出终止该进程的信号编号。
    (b)为假:不是异常终止的
(3)代码演示
      通过判断终止原因、返回值、信号编号,父进程可以决定是否重新运行子进程,不过99%的
    情况是,父进程不关心子进程是怎么终止的,它的返回值是什么。
      有关wait不要去记忆,我们讲wait的主要目的是想告诉你,进程的返回值到底返回给了谁。
(4)wait的缺点
      如果父进程fork创建出了好多子进程,wait只能获取最先终止的那个子进程的“终止”状态
    ,其它的将无法获取,如果你想获取所有子进程终止状态,或者只想获取指定子进程的进程
    终止状态,需要使用wait的兄弟函数waitpid,它们的原理是相似的,所以我们这里不再赘述,
    也不用大家掌握,理解了wait就已经非常到位了。

八、进程状态

进程运行状态有哪些

(a)就绪态:表示进程准备就绪,随时等待被调度运行
(b)执行态:被PID=0的调用进程调度,开始占有CPU,被cpu执行,运行的时间片到后让出CPU,再次进入
就绪态,然后PID==0的调度进程开始调度其它进程,CPU去执行其他进程的指令
(c)阻塞态:当进程需要的某些条件的不满足,进程会进入阻塞态(休眠)
    当条件满足后,会再次进入就绪态,等待再次被调度执行。
(d)终止态:进程终止(正常终止、异常终止)
      如果进程终止时,父进程没有回收进程资源的话,终止的进程会变成僵尸进程。

九、java进程

java属于解释型语言,类似的像c#,python等都是属于解释型语言,而c/c++等则是属于编译型语言。
编译型语言:
    将编译型语言的程序编译后,得到的直接就是机器指令,可以被CPU直接执行。
解释型语言:
    将解释型语言的程序编译后,得到的只是字节码,字节码并不是机器指令,并不能直接被cpu执行
  ,只有当字节码被虚拟机程序解释(翻译)为机器指令后才能被cpu执行,解释的过程也被称为翻译
  的过程。
如何运行编译型和解释型语言的程序
  我们这里讨论的都是有OS支持的情况。  
  运行编译型语言的程序
  (1)父进程(命令行终端窗口、图形界面)会fork复制出子进程空间
  (2)调用exec加载器,直接将编译后代码拷贝到子进程空间
      然后被CPU执行时,整个程序就运行起来了。
    在程序里面,我们自己也可以调用fork和exec函数来执行另外的新程序。
  运行解释型语言的程序
  (1)java程序的运行
    1)父进程(命令行窗口、图形界面)会fork复制出子进程空间
    2)调用exec加载java虚拟机程序,将虚拟机程序的代码拷贝到子进程空间中
      当java虚拟机程序运行起来后,会自动的去解释编译得到的java字节码文件,将字节
    码翻译为机器指令,cpu再去执行翻译得到的机器指令。
      每解释一句,cpu就会执行一句,在虚拟机的翻译下,整个java进程就开始运行起来了。
      其实最简单的理解就是,java虚拟机就代表了java进程。
    当你运行另一个java程序时,又会自动地启动一个虚拟机程序来解释java字节码,此时另一个
  java进程又诞生了。
    也就是说你执行多少个java进程,就会运行多少个java虚拟机,当然java虚拟机程序在硬盘上只有
  一份,只不过被多次启动而已。
  (2)java虚拟机怎么得到
    运行解释型语言的程序时,必须要有虚拟机,不过好在当我们安装java等解释型语言的开发环境时
  ,往往都会自动安装虚拟机程序,我们不用操心。
    当我们运行java程序时,虚拟机会被自动启动。
    虚拟机一般是运行在OS上的,不过其实虚拟机也可以运行在没有OS的裸机上,当虚拟机安装在裸机
  上后,java程序也可以运行在裸机上。
     java字节码文件
        虚拟机
          os
         硬件
      java字节码文件
        虚拟机
         硬件
  (3)在java程序里面,也可以调用java库提供的类似的fork和exec函数,我们自己来创建一个java
子进程,并执行新程序。
      java库提供的类似的fork、exec函数,下层也是调用OS的fork、exec函数。
  (3)思考:虚拟机程序是什么语言写的
    虚拟机程序能够被exec直接加载运行,说明虚拟机必然是编译型语言写的,如果虚拟机使用
  解释性语言来写的话,编译后得到是字节码,字节码不是机器指令,此时还要另外安装一个程
  序来解释虚拟机程序,这就陷入了一个死循环所以java虚拟机必须使用编译型语言来写,比如
  使用c/c++编写,一般来说是c写的。

十、有关程序多进程

10.1 多进程

  一般对于小程序来说,由于体量小,因此基本都是单进程的,在平时的开发中,一般开发的程序都比较小
,特别是大家在学习过程中写的程序,那就更小了,因此基本都是单进程的。
  不过在学习了本章以后,我们完全可以在自己的程序里面调用fork、exec来创建多个进程,特别是当我
们需要多线并发工作时,我们完全可以使用这种多进程的方式来实现。
  当然本章的fork、exec是c接口的,只能供c/c++调用,java等其他语言如果像实现多进程的话,就需
要调用该语言的fork、exec,我们前面说过,这也是基于底层OS的fork和exec来实现的。
(1)程序多进程的例子:同时读鼠标和键盘
    当鼠标和键盘没有数据时,这两个读操作会阻塞,如果在一个进程即读键盘、也读鼠标的话,就会
  出现相互堵的情况。   
      while(1)
      {
        read(键盘);                                 
        read(鼠标);
      } 
      此时就可以fork创建出一个子进程,然后让父子进程并发的读数据。
       父进程             子进程
      read(键盘)         read(鼠标)
      父子进程并发运行,各自读各自的,互不干扰,这样就不会出现相互堵的情况了。

10.2 多线程并发

  在平时的开发中,小型的程序一般都是单进程的,此时很少会使用多进程来并发做事,因为多进程太过
耗费计算机资源,为什么多进程会如此的耗费计算机资源,我们后面讲到线程时再来详说。
  如果程序真的需要并发的话,我们会在进程内部创建多个线程,使用多个线程来实现并发,比如:
     主线程          次线程
    read(键盘)      read(鼠标)
  后面课程会详细的介绍C线程函数。
  疑问:程序多进程会不会用到?
  答:当然会

10.3 什么情况下,我们让程序创建出多进程,实现多进程的并发

  我们前面介绍到,在一般小型应用程序的开发中,程序基本都是单进程的,如果涉及到并发时,往往会
多线程来实现,但这并不是说程序就不存在多进程的情况了,那么在什么情况下,程序会用到多进程呢?
  比如:
(1)编写框架
      安卓的系统框架,中大型游戏底层框架等,软件框架属于结构性的代码,在实现框架结构的
    过程中,基本都要使用多进程来搭建这个框架。
      像框架这种东西,在实际开发中,并不是任何人都会涉及到的,对于绝大多数人来说,在实际
    开发中,基本只涉及到框架的使用,但不涉及到框架的编写。
(2)如果你的程序必须启动新程序
  1)例子1
      比如windows图形界面、命令行,当我们在图形界面或者命令启动新程序时,必须创建子
    进程,然后在子进程空间中运行新代码,这个时候必须要涉及多进程。
  2)例子2
      那些大型的业务功能复杂的软件,基本都是多进程的,为什么?
      这里说的大型软件,有可能是客户端软件,也有可能是服务器端的软件。
      对于大型的软件程序来说,往往包含很多种功能(功能套件),而且每一个功能具有相当的
    独立性,所以大型复杂的软件在实现时,如果将这些独立功能全都放到单进程里面来实现,很
    不现实,因为所有的东西都杂糅在一起,很不好实现。
      既然每个功能具有独立性,在大型软件里面,每个独立的功能,均由独立的进程来实现,其
    中有一个是主进程,主进程往往会提供主的交互界面,主进程启动起来后,会去启动不同的子
    进程,每个子进程负责实现不同的功能。
      在每个子进程的内部往往还会有多线程的参与,所以对于大型软件来说,多进程 和 多线程
    都会涉及。
          主 进 程(提供主交互界面,负责启动子进程)                    
           |         |        |
           |         |        |
           V       V        V
           子       子        子
           进       进        进
           程       程        程
           1        2         3
           某       某        某
           功       功        功
           能       能        能
      典型比如360、鲁大师、WPS、腾讯QQ,在任务管理器里面你会发现,它们有很多的进程
(3)早期服务器程,会通过多进程来支持多用户的访问
           服务器程序
            A     B
            子    子
            进    进
            程    程
            |     |
            |     |  .....
            |     | 
            A     B
            客   客
            户    户
            机    机 
    总之,服务器可以并发运行n多个子进程来应对多客户的访问。
    不过对于现在的服务器来说,更喜欢使用多线程来实现多客户的访问,多进程的情况慢慢比较少了。
  这里只是想告诉你,存在这种情况。
       服务器程序
        A     B
        线    线
        程    程
        |     |
        |     |  .....
        |     | 
        A     B
        客   客
        户    户
        机    机 
    有关多线程,我们后面讲线程时,会详细的讲到。

十一、进程关系

11.1 父子关系

已有进程调用fork创建出一个新的进程,那么这两个进程之间就是父子进程关系,子进程会继承和父
进程的属性。

11.2 进程组

1 什么是进程组
    多个进程可以在一起组成一个进程组,其中某个进程会担任组长,组长进程的pid就是整个进程组
  的组ID。
2 进程组的生命周期
  就算进程组的组长终止了,只要进程中还有一个进程存在,这个进程组就存在。
  进程组从开始被创建,到进程组最后一个进程结束,这段时间就是进程组的生命周期。
3 进程组举例
  当我们自己的程序fork出很多子进程时,所有的进程会默认的为一组,我自己程序中的原始父进程会担
任组长。
  命令行终端窗口与最开始的父进程建立联系,最原始的父进程结束了,就会将终端交互权还给shell程序。
4 相关API
    我们可以调用相关API将一个非组长进程,设置为一个新的进程组组长,或者说加入其它进程组。
  由于这些API在实际开发中基本用不到,所以不用了解。

11.3 会话期关系

多个进程组在一起,就组成了会话期。
  有关会话期我们不做太多讲解,了解有这么个东西存在即可。

十二、守护进程

我们前面介绍过,守护进程也被称为精灵进程。
  对于一些稍大型的软件来说,往往都是多进程的,而且其中好多进程都是默默无闻运行的,像这种默默
无闻运行的进程都需要做成守护进程。
目录
相关文章
|
11天前
|
Linux 应用服务中间件 Shell
linux系统服务二!
本文详细介绍了Linux系统的启动流程,包括CentOS 7的具体启动步骤,从BIOS自检到加载内核、启动systemd程序等。同时,文章还对比了CentOS 6和CentOS 7的启动流程,分析了启动过程中的耗时情况。接着,文章讲解了Linux的运行级别及其管理命令,systemd的基本概念、优势及常用命令,并提供了自定义systemd启动文件的示例。最后,文章介绍了单用户模式和救援模式的使用方法,包括如何找回忘记的密码和修复启动故障。
32 5
linux系统服务二!
|
11天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
39 4
linux进程管理万字详解!!!
|
11天前
|
Linux 应用服务中间件 Shell
linux系统服务!!!
本文详细介绍了Linux系统(以CentOS7为例)的启动流程,包括BIOS自检、读取MBR信息、加载Grub菜单、加载内核及驱动程序、启动systemd程序加载必要文件等五个主要步骤。同时,文章还对比了CentOS6和CentOS7的启动流程图,并分析了启动流程的耗时。此外,文中还讲解了Linux的运行级别、systemd的基本概念及其优势,以及如何使用systemd管理服务。最后,文章提供了单用户模式和救援模式的实战案例,帮助读者理解如何在系统启动出现问题时进行修复。
33 3
linux系统服务!!!
|
2天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
33 8
|
2天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
11 3
|
2天前
|
安全 网络协议 Linux
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。通过掌握 ping 命令,读者可以轻松测试网络连通性、诊断网络问题并提升网络管理能力。
9 3
|
5天前
|
安全 Linux 数据安全/隐私保护
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。本文介绍了使用 `ls -l` 和 `stat` 命令查找文件所有者的基本方法,以及通过文件路径、通配符和结合其他命令的高级技巧。还提供了实际案例分析和注意事项,帮助读者更好地掌握这一操作。
15 6
|
2天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
11 2
|
5天前
|
Linux
在 Linux 系统中,`find` 命令是一个强大的文件查找工具
在 Linux 系统中,`find` 命令是一个强大的文件查找工具。本文详细介绍了 `find` 命令的基本语法、常用选项和具体应用示例,帮助用户快速掌握如何根据文件名、类型、大小、修改时间等条件查找文件,并展示了如何结合逻辑运算符、正则表达式和排除特定目录等高级用法。
22 5
|
6天前
|
机器学习/深度学习 自然语言处理 Linux
Linux 中的机器学习:Whisper——自动语音识别系统
本文介绍了先进的自动语音识别系统 Whisper 在 Linux 环境中的应用。Whisper 基于深度学习和神经网络技术,支持多语言识别,具有高准确性和实时处理能力。文章详细讲解了在 Linux 中安装、配置和使用 Whisper 的步骤,以及其在语音助手、语音识别软件等领域的应用场景。
23 5