一、概念
1.概念
- 进程:是程序执行时的一个实例
- 程序是被存储在磁盘上,包含机器指令和数据的文件
- 当这些指令和数据被装载到内存并被CPU所执行,即形成了进程
- 一个程序可以被同时运行为多个进程
- 在Linux源码中通常将进程称为任务(task)
- 从内核观点看,进程的目的就是担当分配系统资源(CPU时间,内存等)的实体
2.相关命令
pstree 以树状结构显示当前所有进程关系
pstree
ps 以简略方式显示当前用户拥有控制终端的进程信息,可以配合以下选项
- a - 显示所有用户拥有控制终端的进程信息
- x - 包括没有控制终端的进程信息
- u - 以详尽方式显示
- w - 以更大列宽显示
zjh@zjh-virtual-machine:~$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
zjh 1087 0.0 0.1 171208 6272 tty2 Ssl+ 4月19 0:00 /usr/libexec/gdm-wayland-session env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
zjh 1097 0.0 0.4 231860 15872 tty2 Sl+ 4月19 0:00 /usr/libexec/gnome-session-binary --session=ubuntu
zjh 7916 0.0 0.1 19796 4992 pts/0 Ss 12:01 0:00 bash
zjh 10359 0.0 0.0 21344 3328 pts/0 R+ 14:36 0:00 ps u
进程信息列表:
USER:进程的用户ID
PID:进程ID
%CPU:CPU使用率
%MEN:内存使用率
VSZ:占用虚拟内存的大小(KB)
RSS:占用物理内存的大小(KB)
TTY:终端次设备号
STAT:进程状态
- R - 运行,即正在被处理器运行
- S - 可唤醒睡眠,系统中断、获得资源、收到信号,都可唤醒
- D - 不可唤醒睡眠,只能被wake_up系统调用唤醒
- T - 收到SIGTOP(19)信号进入暂停状态,收到SIGCONT(18)信号后继续运行
- Z - 僵尸,已终止但其终止状态未被回收
- < - 高优先级
- N - 低优先级
- L - 存在被锁定的内存分页
- s - 会话首进程
- l - 多线程化进程
- + - 在前台进程组中
START:进程开始时间
TIME:进程运行时间
COMMAND:进程启动命令
二、父子孤尸
父子进程
Unix系统中的进程存在父子关系。一个父进程可以创建一个或多个子进程,但子进程有且只有一个父进程。整个系统中只有一个根进程,即PID = 0的调度进程。
孤儿进程
父进程创建子进程后,子进程在操作系统的调度下与其父进程同时运行,如果父进程先于子进程结束,该子进程即成为孤儿进程,同时孤儿进程被某个特别的进程收养,即成为该专门进程的子进程,于是这个特别的进程又被称为孤儿院进程。
僵尸进程
父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。如果子进程先于父进程终止,但由于某种原因,父进程没有回收该子进程的终止状态,此时子进程即处于僵尸状态,该子进程此时被称为僵尸进程。
僵尸进程虽然已不再活动,即不会继续消耗处理机资源,但其所携带的进程终止状态会消耗内存资源,所以应尽可能及时地回收子进程的僵尸。
三、进程标识符
每个进程都有一个非负整数形式的唯一编号,即PID(Process Identification,进程标识)
PID在任何时刻都是唯一的,但是可以重用,即当一个进程被回收后,它的PID可以为其它进程所用
进程的PID由系统内核根据延迟重用算法生成,以确保新进程的PID不同于最近终止进程的PID
系统中有些PID是专用的,比如:
- 0号进程,调度进程,亦称交换进程(swapper),系统内核的一部分,所有进程的根进程,磁盘上没有它的可执行程序文件
- 1号进程,init进程,在系统自举过程结束时由调度进程创建,读写与系统有关的初始化文件,引导系统至一个特定状态,以超级用户特权运行的普通进程,永不停止
除调度进程以外,系统中的每个进程都有唯一的父进程,对任一子进程而言,其父进程的PID就是它的PPID
四、进程操作
创建子进程
#include<unistd.h>
pid_t fork(void)
功能:创建调用进程的子进程
返回值:创建成功,在父进程中返回子进程的PID,在子进程中返回 0(函数的调用者可以根据返回值的不同,分别为父子进程编写不用的处理分支)
创建失败,返回 -1(系统中总的线程数达到了上限,或者用户的总进程数达到了上限,fork函数会返回失败)
1.为父子进程设计相同的执行过程
int main(void)
{
...;
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
...;//父子进程都执行的代码
return 0;
}
2.为父子进程设计先不同后相同的执行过程
int main(void)
{
...;
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
if(pid == 0)
{
...//子进程执行的代码
}
else
{
...//父进程执行的代码
}
...//父子进程都执行的代码
return 0;
}
3.为父子进程设计不同的执行过程
int main(void)
{
...;
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
if(pid == 0)
{
...//子进程执行的代码
return 0;
}
...//父进程执行的代码
return 0;
}
进程间关系
由fork函数创建的子进程是其父进程的不完全副本,如上图所示,子进程在内存中的映像中,代码区与父进程共享同一块物理内存,其它各区映射到独立的物理内存,其内容从父进程拷贝
fork函数返回后,系统内核会将父进程维护的文件描述符表也复制到子进程的进程表项中,但并不复制文件表项
进程的正常终止
1.从main函数中返回(return)可令进程终止
2.调用exit函数令进程终止
#include <stdlib.h>
void exit(int status)
//status: 进程退出码,相当于main函数返回值
//该函数不返回
exit函数的status参数值虽然是int类型,但只有其中最低数位的字节可被其父进程回收,其它高的 三字节会被忽略。
与通过return语句终止进程相比,return语句只能在main函数中实现,但是exit函数可以在包括 main函数在内的任意函数中调用以终止进程。
exit函数在终止调用进程前会做如下收尾工作:
- 调用实现通过atexit/on_exit函数注册的退出处理函数
- 冲刷并关闭所有仍处于打开状态的标准I/O流
- 删除所有通过tmpfile函数创建的临时文件
- _exit(status)
//注册退出处理函数
#include<stdlib.h>
int atexit(void (*function)(void));
//function: 函数指针,指向退出处理函数
//返回值:成功返回 0,失败返回 -1
int on_exit(void (*function)(int,void*),void* arg);
//function:函数指针,指向退出处理函数。其中:
// 第一个参数来自传递给exit函数的status参数,或在main函数里执行return语句的返回值,
// 第二个参数则来自传递给on_exit函数的arg参数
//arg:泛型指针,将作为第二个参数传递给function所指向的退出处理函数
//返回值:成功返回 0,失败返回 -1
3.调用_exit/_Exit函数令进程终止
#include<unistd.h>
void _exit(int status)
//status: 进程退出码,相当于main函数返回值
//该函数不返回
#include<stdlib.h>
void _Exit(int status)
//status: 进程退出码,相当于main函数返回值
//该函数不返回
_exit函数在终止调用进程前会做如下几件收尾工作,与exit函数所做的不同,事实上,exit函数在做完它那三件收尾工作后紧接着就会调用_exit函数
- 关闭所有仍处于打开状态的文件描述符
- 将调用进程的所有子进程托付给init进程收养
- 向调用进程的父进程发送SIGCHLD(17)信号
- 令调用进程终止运行,将status的低八位(最低数位字节)作为退出码保存在其终止状态
进程的异常终止
1.程序错误或系统故障
当进程执行了某些危险操作,或系统本身发生了某种故障或意外,内核会向相关进程发送特定信号。若进程无意针对收到的信号采取补救措施,那么内核将按照缺省方式将进程杀死,并视情况生成核心转储文件(core)以备事后分析,俗称吐核
- SIGILL(4):进程试图执行非法指令
- SIGBUS(7):硬件或对齐错误
- SIGFPE(8):浮点异常
- SIGSEGV(11):无效内存访问
- SIGPWR(30):系统供电不足
2.人为触发信号
- SIGINT(2):Ctrl + C
- SIGOUT(3):Ctrl + \
- SIGKILL(9):不能被捕获或忽略的进程的终止信号
- SIGTERM(15):可以被捕获或忽略的进程终止信号
3.向进程自己发送信号
abort函数
#include<stdlib.h>
void abort(void);
功能:向调用进程发送SIGABRT(6)信号,该信号默认情况下可使进程结束(无参数,不返回)
回收子进程
1.为什么要回收子进程
- 清楚僵尸进程,避免消耗系统资源
- 父进程需要等待子进程的终止,以继续后续工作
- 父进程需要知道子进程终止的原因(正常终止,需要知道子进程的退出码;异常终止,则需要知道子进程是被哪个信号终止的)
2.wait函数
等待并回收任意子进程
#include <sys/wait.h>
pid_t wait(int* status);
//status:输出子进程的终止状态,可置NULL
//返回值:成功,返回所回收的子进程的PID,失败,返回 -1
父进程在创建若干子进程以后调用wait函数:
- A.若所有子进程都在运行,则阻塞,直到有进程终止才返回
- B.若有一个子进程已终止,则返回该子进程的PID并通过status参数输出其终止状态
- C.若没有任何可被等待并回收的子进程,则返回 -1,置errno为ECHILD
在任何一个子进程终止前,wait函数只能阻塞调用进程,如果有一个子进程在wait函数被调用之前就已经终止并处于僵尸状态,wait函数会立即返回,并取得该子进程的终止状态,同时子进程僵尸消失。由此可见,wait函数主要完成三个任务
- 阻塞父进程的运行,直到子进程终止再继续,停等同步
- 获取子进程的PID和终止状态,令父进程知道子进程的终止原因
- 为子进程收尸,防止大量僵尸进程消耗系统资源
子进程的终止状态通过wait函数中的status参数输出给函数调用者。<sys/wait.h>头文件中提供了辅助分析进程终止状态的工具宏:
WIFEXITED(status)
真:正常终止 WEXITSTATUS(status) -> 进程退出码
假:异常终止 WTERMSIG(status) -> 终止进程的信号
WIFSIGNALED(status)
真:异常终止 WTERMSIG(status) -> 终止进程的信号
假:正常终止 WEXITSTATUS(status) -> 进程退出码
3.waitpid函数
等待回收任意或特定子进程
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int* status,int options);
//pid:-1 等待并回收任意子进程,相当于wait函数
// >0 等待并回收特定子进程
//status:用于输出子进程的终止状态,可置NULL
//options:0 阻塞模式,若所等子进程仍在运行,则阻塞直至该子进程终止
// WNOHANG 非阻塞模式,若所等子进程仍在运行,则返回0
//返回值:成功,返回所回收子进程的PID或者0,失败,返回-1
waitpid(-1,&status,0) <==> wait(&status)
创建新进程
1.exec
与fork函数创建调用进程的子进程不同,exec函数是创建一个新的进程取代调用进程自身。新进程会用自己的全部地址空间覆盖调用进程的地址空间,但进程的PID保持不变。
exec函数族一共包括6个函数:
#include<unistd.h>
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[]);
- l:list,新进程的命令行参数以字符指针列表(const char* arg,...)的形式传入,列表以空指针结束
- p:path,若第一个参数不包含"/",则将其视为文件名,并根据PATH环境变量搜索该文件
- e:environment,新进程的环境变量以字符指针数组(char* const envp[])的形式传入,数组以空指针结束,不指定环境变量则从调用进程复制
- v:vector,新进程的命令行参数以字符指针数组(char* const argv[])的形式传入,数组以空指针结束
调用exec函数不仅改变调用进程的地址空间和进程映像,调用进程的一些属性也发生了变化
- 任何处于阻塞状态的信号都会丢失
- 被设置为捕获的信号会还原为默认操作
- 有关线程属性的设置会还原为缺省值
- 有关进程的统计信息会复位
- 与进程内存相关的任何数据都会丢失,包括内存映射文件
- 标准库在用户空间维护的一切数据结构(如通过atexit或on_exit函数注册的退出处理函数)都会丢失
但也有PID、PPID、实际用户ID和实际组ID、优先级,以及文件描述符等属性会被新进程继承
如果进程创建失败,exec函数会返回-1,如果进程创建成功,exec函数则不会返回(因为成功的exec调用会以跳转到新进程的入口地址作为结束,而刚刚运行的代码是不会存在于新进程的地址空间中的)
如果既想创建新的进程,同时又希望原来的进程继续存在,则可以考虑fork+exec模式,即在fork产生的子进程里调用exec函数,这样,新进程取代了子进程,父进程依然存在
2.system
执行shell命令
#include <stdlib.h>
int system(const char* command);
//command:shell命令行字符串
//返回值:成功,返回command进程的终止状态,失败,返回-1
system函数执行cammand参数所表示的命令行,并返回命令进程的终止状态
若command参数取NULL,返回非0表示Shell可用,返回0表示Shell不可用
system函数内部调用了vfork、exec和waitpid等函数:
- 如果调用vfork或waitpid函数出错,则返回-1
- 如果调用exec函数出错,则在子进程中执行exit(127)
- 如果都成功,则返回command进程的终止状态(由waitpid函数的status参数获得)
与vfork+exec相比,system函数针对各种错误和信号都做了必要处理,而且system是标准库函数,可跨平台使用