进程基础
1.进程概述
- 进程在运行过程中,程序内部所有的指令都是通过CPU处理完成的,CPU只进行数据运算并不具备数据存储的能力,其处理的数据都加载自物理内存。这些数据是通过CPU中的内存管理单元MMU从进程的虚拟地址空间中映射过去的。
- 进程控制块(PCB:processing control block):进程控制块位于进程虚拟地址空间中,内核区内,本质上是一个叫做task_struct的结构体。下面是一些常用的信息:
- 进程id:每一个进程都有一个唯一的进程ID,类型为pid_t,本质是一个整形数
- 进程的状态:进程有不同的状态,状态是一直在变化的,有就绪、运行、挂起、停止等状态。
- 进程对应的虚拟地址空间的信息。
- 描述控制终端的信息,进程在哪个终端启动默认就和哪个终端绑定。
- 当前工作目录:默认情况下,启动进程的目录就是当前的工作目录
- umask 掩码:在创建新文件的时候,通过这个掩码屏蔽某些用于对文件的操作权限。
- 文件描述符表:每个被分配的文件描述符都对应一个已经打开的磁盘文件
- 和信号相关的信息:在Linux中调用函数、键盘快捷键、执行shell命令等操作都会产生信号。
- 阻塞信号集:记录当前进程中阻塞哪些已产生的信号,使其不能被处理
- 未决信号集:记录在当前进程中产生的哪些信号还没有被处理掉。
- 用户id和组id:当前进程属于哪个用户,属于哪个用户组
- 会话(Session)和进程组:多个进程的集合叫进程组,多个进程组的集合叫会话。
- 进程可以使用的资源上限:可以使用命令
ulimit -a
查看详细信息。
- 进程的状态:进程一共有五种状态,分别是创建态、就绪态、运行态、阻塞态、终止态。
- 阻塞态(挂起态):进程被强制放弃CPU,并且没有抢夺CPU时间片的资源。比如说在程序中调用了sleep函数
- 进程相关的命令
- 查看进程:使用ps命令,其常用参数如下:
# -a:查看所有终端的信息 # -u:查看用户相关的信息 # -x:显式和终端无关的进程信息
- 查看Linux中的标准信号:kill -l
- 发送指定的信号到对应的进程,使用kill命令
# 9即表示SIGKILL信号, 进程收到SIGKILL信号会无条件杀死进程 kill -9 进程ID kill -SIGKILL 进程ID
2.进程的创建以及进程id的获取
- 获取当前进程的进程ID(即PID)
#include <unistd.h>
pid_t getpid (void)
- 获取当前进程的父进程ID(即PPID)
#include <unistd.h>
pid_t getppid (void)
- 通过复制进程映像,创建一个新的进程,使用fork系统调用
- 函数原型如下
#include <unistd.h> pid_t fork (void)
- 该函数调用成功后,从一个虚拟地址空间变成了两个虚拟地址空间。每个地址空间中都会将fork调用的返回值记录下来。父进程的虚拟地址空间中将该返回值标记为一个大于0的数(即子进程的进程ID);子进程的虚拟地址空间中将该返回值标记为0。程序中需要通过fork调用的返回值判断当前进程是子进程还是父进程。
- 示例:
#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.父子进程
- 进程的执行位置:在父进程中成功创建了子进程,子进程就拥有父进程代码区的所有代码,那么父子进程分别在什么位置开始执行的?父进程从main函数开始执行,子进程是在父进程调用fork函数之后被创建,子进程就从fork调用之后开始向下执行代码。
- 循环创建子进程:在一个父进程中循环创建三个子进程,最终得到四个进程。例子如下:
#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
- 终端显示问题:
- 问题描述:
- 原因分析:
- a.out 进程启动之后,创建了子进程,其实a.out也是有父进程的就是当前的终端
- 终端只能检测到a.out进程的状态,a.out执行期间终端切换到后台,a.out 执行完毕之后终端切换回前台
- 当终端切换到前台之后,a.out的子进程还没有执行完毕,子进程输出的信息就会显示到终端命令提示符的后边了,导致终端显示有问题,但是此时终端是可以接收键盘输入的,只是看起来不美观而已。
- 问题解决:让所有子进程退出之后再退出父进程。在父进程代码中调用sleep()
- 问题描述:
- 两个进程中是不能通过全局变量实现数据交互的,因为每个进程都有自己的地址空间。如果需要进行进程之间通信,需要使用管道、共享内存、本地套接字、内存映射区、消息队列等方式。
4.exec族函数
- 需求:通过现在运行的进程启动磁盘上的另一个可执行程序,也就是通过一个进程启动另一个进程(替换进程映像),这种情况下我们可以使用 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[]);
- exec族函数中最常用的有两个 execl() 和 execlp()
- execl函数:用于执行任意一个可执行程序。函数原型如下所示:
#include <unistd.h> int execl(const char *path, const char *arg, ...); 参数: path: 要启动的可执行程序的路径 arg和...:表示启动的可执行程序名称以及其参数,等同于arg[0],arg[1]... 返回值:如果这个函数执行成功,没有返回值,如果执行失败,返回 -1
- execlp函数:用于执行已经设置了PATH环境变量的可执行程序。这个函数会自动搜索系统的环境变量PATH,因此使用这个函数执行可执行程序不需要指定路径,只需要指定出名称即可。函数原型如下:
// p == path int execlp(const char *file, const char *arg, ...); 参数 file:可执行程序的名字 arg和...:表示启动的可执行程序名称以及其参数,等同于arg[0],arg[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.进程控制
进程控制主要是指进程的退出、进程的回收、进程的状态
- 结束进程:退出某个进程可以调用
exit()
函数或者在main函数中直接使用return关键字退出进程。 - 进程的状态之孤儿进程:在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称之为孤儿进程。当OS检测到某一个进程变成了孤儿进程,这时候系统就会有一个固定的进程(例如进程号为1的init进程)领养这个孤儿进程,进行资源的释放。
- 孤儿进程示例
#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; }
- 运行上述程序,然后运行
pstree 1
就可以看到孤儿进程被进程号为1的进程领养
- 进程的状态之僵尸进程:在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行,子进程先于父进程结束,子进程无法释放自己的 PCB 资源,需要父进程来做这个件事儿,但是如果父进程也不管,这时候子进程就变成了僵尸进程。
- 僵尸进程的示例
#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; }
- 查看僵尸进程:使用
ps -aux | grep 可执行程序名称
查看进程的状态。如下所示,其中z表示zomie,僵尸
- 杀死僵尸进程:使用kill命令杀死僵尸进程的父进程,
kill -9 僵尸进程的父进程ID
- 进程的回收:为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收,回收方式有多种,一种是阻塞方式wait(),一种是非阻塞方式waitpid(),还有就是利用SIGCHLD信号。
- wait:这是一个阻塞函数,会阻塞调用进程执行,直到子进程终止。子进程终止后,回收子进程的资源。这个函数被调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。
- 函数原型如下:
#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:表示没有子进程可以回收
- 通过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; }
- waitpid:该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。
- 函数原型:
#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
- 使用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; }
- 利用子进程退出会产生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; }
- wait:这是一个阻塞函数,会阻塞调用进程执行,直到子进程终止。子进程终止后,回收子进程的资源。这个函数被调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。
守护进程
守护进程,全称daemon process,他是Linux中的后台服务进程,一般周期性的运行。守护进程的程序名称一般以*d
的形式,比如MySQL服务:mysqld
1.进程组
- 进程组:多个进程的集合。这个组中必须有一个组长,组长就是进程组中的第一个进程,组长以外的都是普通的成员,每个进程组都有一个唯一的组ID,即进程组的ID(PGID) 和组长的PID是一样的。
- 几个常用的进程组函数:获取或者设置进程组的id
- 获取当前进程所在的进程组的组id:getpgrp函数
- 获取指定的进程所在的进程组的组id,参数pid就是指定的进程
pid_t getpgid(pid_t pid) 函数参数: pid:进程id,如果为0表示获取调用进程的进程组id 函数返回值:返回pid的进程组id
- 将某个进程移动到其他进程组或者创建新的进程组
int setpgid(pid_t pid, pid_t pgid); 函数参数: pid: 某个进程的进程 ID pgid: 某个进程组的组 ID 如果pgid对应的进程组存在,pid对应的进程会移动到这个组中 如果pgid对应的进程组不存在,则会创建一个新的进程组,进程组id为pid 返回值:函数调用成功返回 0,失败返回 - 1
2.会话
- 会话 (session) 是由一个或多个进程组组成的,一个会话可以对应一个控制终端,也可以没有。一个普通的进程可以调用setsid()函数使自己成为新session的领头进程(会长),并且这个session领头进程还会被放入到一个新的进程组中,成为进程组的组长。
- 会话相关函数
#include <unistd.h>
// 获取某个进程所属的会话ID
pid_t getsid(pid_t pid);
// 创建会话并设置进程组id。创建会话时,调用这个函数的进程不能是组长进程
// 调用这个函数的进程将成为新会话的会长,成为新进程组的组长
pid_t setsid(void);
函数返回值:
成功返回新进程组的id即调用进程的id
失败返回-1。如果错误码为EPERM,表示调用的进程已经是进程组的组长
3.守护进程
- 守护进程:它是Linux下的后台服务进程,一般独立于控制终端周期性地运行。常常在系统启动后运行,系统关闭时才终止。
- 守护进程的创建流程
- 创建子进程,让父进程退出。因为父进程可能是组长进程,组长进程不能调用setsid函数创建新会话
- 通过子进程使用setsid函数创建新的会话
- 改变当前进程的工作目录,使用chdir函数
- 使用umask函数重新设置文件的掩码,去掉文件的某些权限
- 关闭或者重定向文件描述符
- 关闭文件描述符:
close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO);
- 重定向文件描述符
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使用率过高问题的排查
- 使用top命令查看进程中的哪一个线程CPU占用率高
top -H
- 使用pstack命令查看进程中各个线程的调用堆栈情况
# 首先查看运行服务的进程号
ps -ef | grep 服务名称
pstack pid
进程通信
这一节内容,参见Linux之进程通信。