进程基础

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
  1. 终端显示问题:
    1. 问题描述:
      image.png
    2. 原因分析:
      1. a.out 进程启动之后,创建了子进程,其实a.out也是有父进程的就是当前的终端
      2. 终端只能检测到a.out进程的状态,a.out执行期间终端切换到后台,a.out 执行完毕之后终端切换回前台
      3. 当终端切换到前台之后,a.out的子进程还没有执行完毕,子进程输出的信息就会显示到终端命令提示符的后边了,导致终端显示有问题,但是此时终端是可以接收键盘输入的,只是看起来不美观而已。
    3. 问题解决:让所有子进程退出之后再退出父进程。在父进程代码中调用sleep()
  2. 两个进程中是不能通过全局变量实现数据交互的,因为每个进程都有自己的地址空间。如果需要进行进程之间通信,需要使用管道、共享内存、本地套接字、内存映射区、消息队列等方式。
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之进程通信