进程基础

1.进程概述
  1. 进程在运行过程中,程序内部所有的指令都是通过CPU处理完成的,CPU只进行数据运算并不具备数据存储的能力,其处理的数据都加载自物理内存。这些数据是通过CPU中的内存管理单元MMU从进程的虚拟地址空间中映射过去的。
  2. 进程控制块(PCB:processing control block):进程控制块位于进程虚拟地址空间中,内核区内,本质上是一个叫做task_struct的结构体。下面是一些常用的信息:
    1. 进程id:每一个进程都有一个唯一的进程ID,类型为pid_t,本质是一个整形数
    2. 进程的状态:进程有不同的状态,状态是一直在变化的,有就绪、运行、挂起、停止等状态。
    3. 进程对应的虚拟地址空间的信息。
    4. 描述控制终端的信息,进程在哪个终端启动默认就和哪个终端绑定。
    5. 当前工作目录:默认情况下,启动进程的目录就是当前的工作目录
    6. umask 掩码:在创建新文件的时候,通过这个掩码屏蔽某些用于对文件的操作权限。
    7. 文件描述符表:每个被分配的文件描述符都对应一个已经打开的磁盘文件
    8. 和信号相关的信息:在Linux中调用函数、键盘快捷键、执行shell命令等操作都会产生信号。
    9. 阻塞信号集:记录当前进程中阻塞哪些已产生的信号,使其不能被处理
    10. 未决信号集:记录在当前进程中产生的哪些信号还没有被处理掉。
    11. 用户id和组id:当前进程属于哪个用户,属于哪个用户组
    12. 会话(Session)和进程组:多个进程的集合叫进程组,多个进程组的集合叫会话。
    13. 进程可以使用的资源上限:可以使用命令ulimit -a查看详细信息。
  3. 进程的状态:进程一共有五种状态,分别是创建态、就绪态、运行态、阻塞态、终止态。
    1. 阻塞态(挂起态):进程被强制放弃CPU,并且没有抢夺CPU时间片的资源。比如说在程序中调用了sleep函数
  4. 进程相关的命令
    1. 查看进程:使用ps命令,其常用参数如下:
    # -a:查看所有终端的信息
    # -u:查看用户相关的信息
    # -x:显式和终端无关的进程信息
    
    1. 查看Linux中的标准信号:kill -l
    2. 发送指定的信号到对应的进程,使用kill命令
    # 9即表示SIGKILL信号, 进程收到SIGKILL信号会无条件杀死进程
    kill -9 进程ID
    kill -SIGKILL 进程ID
    
2.进程的创建以及进程id的获取
  1. 获取当前进程的进程ID(即PID)
#include <unistd.h>
pid_t getpid (void)
  1. 获取当前进程的父进程ID(即PPID)
#include <unistd.h>
pid_t getppid (void)
  1. 通过复制进程映像,创建一个新的进程,使用fork系统调用
    1. 函数原型如下
    #include <unistd.h>
    pid_t fork (void)
    
    1. 该函数调用成功后,从一个虚拟地址空间变成了两个虚拟地址空间。每个地址空间中都会将fork调用的返回值记录下来。父进程的虚拟地址空间中将该返回值标记为一个大于0的数(即子进程的进程ID);子进程的虚拟地址空间中将该返回值标记为0。程序中需要通过fork调用的返回值判断当前进程是子进程还是父进程。
    2. 示例:
    #include <unistd.h>
    #include <iostream>
    using namespace std;
    int main() {
        // 在父进程中创建子进程
        pid_t pid = fork();
        if (pid > 0) {
            // 父进程执行的逻辑
            printf("父进程,pid=%d\n", getpid());
        } else if (pid == 0) {
            // 子进程执行逻辑
            printf("子进程,pid=%d\n", getpid());
        } else {
            // 创建子进程失败
        }
        // 父子进程都会执行的逻辑
        for (int i = 0; i < 5; i++) {
            printf("%d\n", i);
        }
        return 0;
    }
    
3.父子进程
  1. 进程的执行位置:在父进程中成功创建了子进程,子进程就拥有父进程代码区的所有代码,那么父子进程分别在什么位置开始执行的?父进程从main函数开始执行,子进程是在父进程调用fork函数之后被创建,子进程就从fork调用之后开始向下执行代码。
  2. 循环创建子进程:在一个父进程中循环创建三个子进程,最终得到四个进程。例子如下:
#include <iostream>
#include <unistd.h>
using namespace std;

int main() {
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        printf("当前进程PID,%d\n", getpid());
        // 子进程不继续创建进程
        if (pid == 0) break;
    }
    return 0;
}

当前进程PID,139517
当前进程PID,139517
当前进程PID,139517
当前进程PID,139520
当前进程PID,139519
当前进程PID,139518
终端显示问题: 问题描述:
image.png
  • 原因分析:
    1. a.out 进程启动之后,创建了子进程,其实a.out也是有父进程的就是当前的终端
    2. 终端只能检测到a.out进程的状态,a.out执行期间终端切换到后台,a.out 执行完毕之后终端切换回前台
    3. 当终端切换到前台之后,a.out的子进程还没有执行完毕,子进程输出的信息就会显示到终端命令提示符的后边了,导致终端显示有问题,但是此时终端是可以接收键盘输入的,只是看起来不美观而已。
  • 问题解决:让所有子进程退出之后再退出父进程。在父进程代码中调用sleep()
  • 两个进程中是不能通过全局变量实现数据交互的,因为每个进程都有自己的地址空间。如果需要进行进程之间通信,需要使用管道、共享内存、本地套接字、内存映射区、消息队列等方式。
  • 4.exec族函数
    1. 需求:通过现在运行的进程启动磁盘上的另一个可执行程序,也就是通过一个进程启动另一个进程(替换进程映像),这种情况下我们可以使用 exec族函数。这些函数执行成功之后不会返回,因为调用进程的实体包括代码段、数据段、和堆栈等都已经被新的内容取代。 这些函数原型如下:
    #include <unistd.h>
    
    extern char **environ;
    int execl(const char *path, const char *arg, ...
              /* (char  *) NULL */);
    int execlp(const char *file, const char *arg, ...
               /* (char  *) NULL */);
    int execle(const char *path, const char *arg, ...
               /*, (char *) NULL, char * const envp[] */);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[],
                char *const envp[]);
    
    1. exec族函数中最常用的有两个 execl() 和 execlp()
      1. execl函数:用于执行任意一个可执行程序。函数原型如下所示:
      #include <unistd.h>
      int execl(const char *path, const char *arg, ...);
      参数:
          path: 要启动的可执行程序的路径
          arg和...:表示启动的可执行程序名称以及其参数,等同于arg[0],arg[1]...
      返回值:如果这个函数执行成功,没有返回值,如果执行失败,返回 -1
      
      1. execlp函数:用于执行已经设置了PATH环境变量的可执行程序。这个函数会自动搜索系统的环境变量PATH,因此使用这个函数执行可执行程序不需要指定路径,只需要指定出名称即可。函数原型如下:
      // p == path
      int execlp(const char *file, const char *arg, ...);
      参数
          file:可执行程序的名字
          arg和...:表示启动的可执行程序名称以及其参数,等同于arg[0],arg[1]...
      返回值:如果这个函数执行成功,没有返回值,如果执行失败,返回 -1
      
      1. 示例:一般调用exec族函数的时候,都会先创建一个子进程,在子进程中调用exec族函数,子进程的用户区数据被替换掉开始执行新的程序中的代码逻辑,但是父进程不受任何影响仍然可以继续正常工作。
      #include <stdio.h>
      #include <unistd.h>
      using namespace std;
      
      int main() {
          pid_t pid = fork();
          // 子进程
          if (pid == 0) {
              int ret = execl("/usr/bin/ps", "ps", "au", NULL);
              if (ret == -1)
                  perror("execl execute error\n");
          } else if (pid > 0) {
              printf("父进程\n");
              sleep(1);
          }
          
          return 0;
      }
      
    5.进程控制

    进程控制主要是指进程的退出、进程的回收、进程的状态

    1. 结束进程:退出某个进程可以调用exit()函数或者在main函数中直接使用return关键字退出进程。
    2. 进程的状态之孤儿进程:在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称之为孤儿进程。当OS检测到某一个进程变成了孤儿进程,这时候系统就会有一个固定的进程(例如进程号为1的init进程)领养这个孤儿进程,进行资源的释放。
      1. 孤儿进程示例
      #include <unistd.h>
      #include <stdio.h>
      
      int main() {
          pid_t pid = fork();
          if (pid ==0) {
              // 保证主进程先执行完并退出
              while (1) {
                  sleep(1);
                  printf("子进程:%d,%d\n", getpid(), getppid());
              }
          }
          else if (pid > 0) {
              printf("主进程:%d\n", getpid());
          }
          return 0;
      }
      
      1. 运行上述程序,然后运行pstree 1就可以看到孤儿进程被进程号为1的进程领养
    3. 进程的状态之僵尸进程:在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行,子进程先于父进程结束,子进程无法释放自己的 PCB 资源,需要父进程来做这个件事儿,但是如果父进程也不管,这时候子进程就变成了僵尸进程。
      1. 僵尸进程的示例
      #include <unistd.h>
      #include <stdio.h>
      
      int main() {
          pid_t pid = fork();
          if (pid == 0) {
              printf("子进程id:%d\n", getpid());
          } else if (pid > 0) {
              // 保证主进程不退出
              while (1) {
      
              }
          }
          return 0;
      }
      
      1. 查看僵尸进程:使用ps -aux | grep 可执行程序名称查看进程的状态。如下所示,其中z表示zomie,僵尸
      2. 杀死僵尸进程:使用kill命令杀死僵尸进程的父进程,kill -9 僵尸进程的父进程ID
    4. 进程的回收:为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收,回收方式有多种,一种是阻塞方式wait(),一种是非阻塞方式waitpid(),还有就是利用SIGCHLD信号。
      1. wait:这是一个阻塞函数,会阻塞调用进程执行,直到子进程终止。子进程终止后,回收子进程的资源。这个函数被调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。
        1. 函数原型如下:
        #include <sys/wait.h>
        
        pid_t wait(int *status);
        函数参数:
            status:传出参数,通过传递出的信检查息回收的进程的状态。具体是使用以下宏进行检查
                WIFEXITED(status): 返回 1, 表明进程是正常退出的
                    WEXITSTATUS(status):获取进程退出时的状态码,相当于调用exit或者return传递的数值。这个宏和WIFEXITED结合使用
                WIFSIGNALED(status): 返回 1, 子进程是被信号终止
                    WTERMSIG(status):获取子进程是被哪个信号杀死的,会得到信号的编号。这个宏和WIFSIGNALED结合使用
        返回值:
            成功:返回被回收的子进程的进程 ID
            失败: -1
                错误码为ECHILD:表示没有子进程可以回收
               
        
        1. 通过wait函数回收多个子进程资源示例:
        #include <stdio.h>
        #include <sys/wait.h>
        #include <unistd.h>
        #include <stdlib.h>
        #include <errno.h>
        
        int main() {
            pid_t pid;
            // 创建五个子进程
            for (int i = 0; i < 5; i++) {
                pid = fork();
                // 子进程不继续创建子进程
                if (pid == 0) break;
            }
            // 父进程
            if (pid > 0) {
                int status;
                while (1) {
                    int ret = wait(&status);
                    if (ret > 1) {
                        if (WIFEXITED(status)) {
                            printf("status:%d\n", WEXITSTATUS(status));
                            printf("回收子进程资源成功,子进程id:%d\n", ret);
                        }
                        
                    } else if (ret == -1 && errno == ECHILD) {
                        // 无子进程需要回收
                        break;
                    }
                }
                printf("over\n");
        
            } else if (pid == 0) {
                printf("子进程id:%d,父进程id:%d\n", getpid(), getppid());
                exit(10);
            }
        
            
            return 0;
        }
        
      2. waitpid:该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。
        1. 函数原型:
        #include <sys/wait.h>
        pid_t waitpid(pid_t pid, int *status, int options);
        
        参数:
        pid:
            -1:回收所有的子进程资源,和wait()是一样的,一次性回收一个子进程
            大于0:指定回收某一个子进程的资源 ,pid是要回收的子进程的进程ID
            0:回收当前进程所在进程组的所有子进程
            小于 -1:pid的绝对值代表进程组ID,表示要回收这个进程组的所有子进程资源
        status: NULL, 和 wait 的参数是一样的
        options: 控制函数是阻塞还是非阻塞
            0: 函数行为是阻塞的,和wait一样
            WNOHANG: 函数行为是非阻塞的
            
        返回值:
            如果函数是非阻塞的,没有要死亡的子进程,返回 0
            成功:得到子进程的进程 ID
            失败: -1
                没有子进程资源可以回收了,函数如果是阻塞的,阻塞会解除,直接返回 - 1
        
        1. 使用waitpid阻塞回收多个子进程资源,实现与wait函数同样的功能
        #include <stdio.h>
        #include <unistd.h>
        #include <sys/wait.h>
        #include <stdlib.h>
        
        int main() {
            pid_t pid;
            // 创建五个子进程
            for (int i = 0; i < 5; i++) {
                pid = fork();
                if (pid == 0) break;
            }
        
            if (pid > 0) {
                int status;
                while (1) {
                    pid_t ret = waitpid(0, &status, 0);
                    if (ret > 0) {
                        printf("成功回收了子进程资源,子进程PID:%d\n", ret);
                        if (WIFEXITED(status)) {
                            printf("子进程退出的状态码为:%d\n", WEXITSTATUS(status));
        
                        }
                        if (WIFSIGNALED(status)) {
                            printf("子进程被信号杀死:%d\n", WTERMSIG(status));
                        }
                    } else {
                        printf("回收失败或者已经没有子进程了\n");
                        break;
                    }
        
                }
            } else if (pid == 0) {
                printf("子进程id:%d,父进程id:%d\n", getpid(), getppid());
                exit(10);
            }
            
            return 0;
        }
        
      3. 利用子进程退出会产生SIGCHLD信号发送给父进程这一特性以及waitpid实现无阻塞回收子进程的资源。
      #include <stdio.h>
      #include <signal.h>     // for sigaction,etc
      #include <unistd.h>     // for fork
      #include <sys/wait.h>   // for waitpid
      
      void handler(int num) {
          printf("captured signal:%d\n", num);
          while(1) {
              pid_t pid = waitpid(-1, NULL, WNOHANG);
              if(pid > 0)
              {
                  printf("child died, pid = %d\n", pid);
              }
              else if(pid == 0)
              {
                  // 没有死亡的子进程, 直接退出当前循环
                  break;
              }
              else if(pid == -1)
              {
                  printf("over,all child process died\n");
                  break;
              }
          }
      }
      
      int main() {
          // 初始化阻塞信号集
          sigset_t set;
          sigemptyset(&set);
          sigaddset(&set, SIGCHLD);
          // 修改调用进程的阻塞信号集
          sigprocmask(SIG_BLOCK, &set, NULL);
      
          // 创建五个子进程
          pid_t pid;
          for (int i = 0; i < 5; ++i) {
              pid = fork();
              if (pid == 0) break;
          }
      
          if (pid > 0) {
              // 父进程
              sleep(10); // 让子进程先结束运行,模拟子进程先发出SIGCHLD信号,主进程后注册信号处理器的情况
              struct sigaction act;
              act.sa_flags = 0;
              act.sa_handler = handler; 
              // 不屏蔽任何信号
              sigemptyset(&act.sa_mask);
              sigaction(SIGCHLD, &act, NULL);
      
              // 解除SIGCHLD信号的阻塞,未决信号集中的SIGCHLD标志位就会从1变为0
              sigprocmask(SIG_UNBLOCK, &set, NULL);
              while (1) {
      
              }
          
          } else if (pid == 0) {
              // 子进程
              printf("%d,%d\n", getpid(), getppid());
          }
          
          return 0;
      }
      

    守护进程

    守护进程,全称daemon process,他是Linux中的后台服务进程,一般周期性的运行。守护进程的程序名称一般以*d的形式,比如MySQL服务:mysqld

    1.进程组
    1. 进程组:多个进程的集合。这个组中必须有一个组长,组长就是进程组中的第一个进程,组长以外的都是普通的成员,每个进程组都有一个唯一的组ID,即进程组的ID(PGID) 和组长的PID是一样的。
    2. 几个常用的进程组函数:获取或者设置进程组的id
      1. 获取当前进程所在的进程组的组id:getpgrp函数
      2. 获取指定的进程所在的进程组的组id,参数pid就是指定的进程
      pid_t getpgid(pid_t pid)
      函数参数:
          pid:进程id,如果为0表示获取调用进程的进程组id
      函数返回值:返回pid的进程组id
          
      
      1. 将某个进程移动到其他进程组或者创建新的进程组
      int setpgid(pid_t pid, pid_t pgid);
      函数参数:
          pid: 某个进程的进程 ID
          pgid: 某个进程组的组 ID
              如果pgid对应的进程组存在,pid对应的进程会移动到这个组中
              如果pgid对应的进程组不存在,则会创建一个新的进程组,进程组id为pid
      返回值:函数调用成功返回 0,失败返回 - 1
      
    2.会话
    1. 会话 (session) 是由一个或多个进程组组成的,一个会话可以对应一个控制终端,也可以没有。一个普通的进程可以调用setsid()函数使自己成为新session的领头进程(会长),并且这个session领头进程还会被放入到一个新的进程组中,成为进程组的组长。
    2. 会话相关函数
    #include <unistd.h>
    
    // 获取某个进程所属的会话ID
    pid_t getsid(pid_t pid);
    
    // 创建会话并设置进程组id。创建会话时,调用这个函数的进程不能是组长进程
    // 调用这个函数的进程将成为新会话的会长,成为新进程组的组长
    pid_t setsid(void);
    函数返回值:
        成功返回新进程组的id即调用进程的id
        失败返回-1。如果错误码为EPERM,表示调用的进程已经是进程组的组长
    
    3.守护进程
    1. 守护进程:它是Linux下的后台服务进程,一般独立于控制终端周期性地运行。常常在系统启动后运行,系统关闭时才终止。
    2. 守护进程的创建流程
      1. 创建子进程,让父进程退出。因为父进程可能是组长进程,组长进程不能调用setsid函数创建新会话
      2. 通过子进程使用setsid函数创建新的会话
      3. 改变当前进程的工作目录,使用chdir函数
      4. 使用umask函数重新设置文件的掩码,去掉文件的某些权限
      5. 关闭或者重定向文件描述符
        1. 关闭文件描述符:
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);
        
        1. 重定向文件描述符
        int fd;
        fd = open("/dev/null", O_RDWR, 0);
        if (fd != -1)
        {
            dup2(fd, STDIN_FILENO);
            dup2(fd, STDOUT_FILENO);
            dup2(fd, STDERR_FILENO);
        }
        

    进程CPU使用率过高问题的排查

    1. 使用top命令查看进程中的哪一个线程CPU占用率高
    top -H 
    
    1. 使用pstack命令查看进程中各个线程的调用堆栈情况
    # 首先查看运行服务的进程号
    ps -ef | grep 服务名称
    pstack pid
    

    进程通信

    这一节内容,参见Linux之进程通信