建站提交历史文章,原文写作时间 2023 年 2 月前后。
进程基本控制
进程状态命令
查看与设置进程资源限制
查看进程状态快照
STAT状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 状态: R 运行或就绪 S 可中断的睡眠 D 不可中断的睡眠 (usually IO) Z 僵尸进程 (nearly dead) X 已死 (should never be seen) I 空闲的内核线程 T 停止或被追踪 t 追踪时被调试器停止 其他: < 高优先级 (not nice to other users) N 低优先级 (nice to other users) s 拥有子进程 l 拥有多线程 + 前台进程
监视实时进程状态
按键操作:
h:相关帮助。
P:按CPU占用排序。
M:按内存占用排序。
T:按运行时长排序。
U:按用户名筛选。
k:杀死进程,输入无效数据退出。
q:退出top。
杀死进程
1 2 kill <pid> kill -9 <pid>
查看与设置进程号
getpid,getppid
关于进程组与会话相关更多内容见2.4.1 进程、进程组、会话章节。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <sys/types.h> #include <unistd.h> pid_t getpid (void ) ;pid_t getppid (void ) ;
创建子进程
创建进程分叉
进程分叉将创建当前进程(父进程)的拷贝。进程分叉初始,父进程与子进程具有完全相同的状态:两者共享相同代码,运行到代码的相同位置,具有完全相同的数据集。当然,两者的进程标识pid与ppid是不同的。
返回值:进程分叉将有两个返回值,一个来自父进程,一个来自子进程。
在父进程中,返回子进程的pid。
在子进程中,返回0。
如果进程创建异常,不会产生子进程,返回-1。
1 2 3 4 #include <sys/types.h> #include <unistd.h> pid_t fork (void ) ;
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main () { printf ("Fork test:\n" ); int pid = fork(); if (pid == -1 ) { perror ("fork" ); return -1 ; } else if (pid) { printf ("This is parent process, pid = %d, ppid = %d\n" , getpid (), getppid ()); sleep (1 ); } else { printf ("This is child process, pid = %d, ppid = %d\n" , getpid (), getppid ()); sleep (1 ); } for (int i = 1 ; i <= 3 ; i++) { printf ("%d\t%d\n" , getpid (), i); } return 0 ; }
输出:
1 2 3 4 5 6 7 8 9 Fork test: This is parent process, pid = 10001 , ppid = 1506 This is child process, pid = 10086 , ppid = 10001 10001 1 10086 1 10001 2 10086 2 10086 3 10001 3
注:由于fork直译为分叉,所以该进程创建又称为进程分叉。
进程分叉内存管理原理
fork函数将进程进行拷贝,生成一个完全相同的进程。
由于fork函数生成一个完全相同的进程,因此子进程将与父进程有相同的虚拟地址映射表。换言之,子进程与父进程的虚拟地址映射到相同的物理地址,但是两者在数据上应该具有独立性,这导致了子进程与父进程对当前数据只有读的权限,不具备写的权限。如果进程对物理地址的值进行修改,将导致两者的数据同时发生修改,这不是期望发生的。
进程分叉运用了写时拷贝的技术,具备了写的权限,同时为进程分叉节约了大量的时间与空间。在进程分叉的初始状态,子进程与父进程的虚拟内存指向相同的真实内存空间,当子进程与父进程的任意一方需要修改时,修改部分的虚拟内存会指向一片新的真实内存,并赋予这个空间新的值。这样做的好处是,例如代码和一些无需更改的数据,将不会重复占用真实内存。
退出进程
进程退出的方法有三种:_exit(0)、exit(0)(推荐)、main函数中的return 0。
main函数中的return 0实际上是调用了exit(0)。
exit函数是标准C库函数,是对Linux函数_exit的封装。
1 2 3 4 5 6 7 8 9 10 11 #include <unistd.h> void _exit(int status);#include <stdlib.h> void exit (int status) ;
GDB 多进程调试
关闭detach-on-fork才能在调试中切换进程。
1 2 (gdb) show detach-on-fork (gdb) set detach-on-fork off
设置follow-fork-mode跟踪起始进程。
1 2 (gdb) show follow-fork-mode (gdb) set follow-fork-mode parent
切换跟踪进程。
1 2 3 4 5 (gdb) info inferiors (gdb) inferior <ID>
exec 函数簇重载进程
execl、execlp、execle、execv、execvp、execvpe
exec函数簇将重载进程,直接覆盖当前进程代码,因此exec函数簇通常需要在fork进程中执行。
l:参数使用列表传递,传递一系列参数,以NULL参数(哨兵)结尾。
p:在环境变量和当前路径中查找file,其他函数仅在当前路径查找。
e:传递envp数组指定路径搜索目录,此时哨兵显得尤为重要。
v:参数使用argv数组传递,而不是参数列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <unistd.h> int execl (const char *pathname, const char *arg, ...) ;int execlp (const char *file, const char *arg, ...) ;int execle (const char *pathname, const char *arg, ...) ;int execv (const char *pathname, char *const argv[]) ;int execvp (const char *file, char *const argv[]) ;int execvpe (const char *file, char *const argv[], char *const envp[]) ;
注:根据习惯,第一个参数为文件名,即本身。如ps aux参数为ps、aux。
注:如果执行成功没有返回值,因为当前进程代码已覆盖,无法继续执行。
注:system 函数实际是对 fork 函数与 exec 函数簇的封装,通常使用 system 函数可以更方便的满足需求。
回收子进程
孤儿进程与僵尸进程
孤儿进程:父进程结束、子进程尚未结束,此时子进程为孤儿进程,init进程(PID为1)代替父进程托管子进程。孤儿进程虽然脱离父进程控制,但是会自然结束,通常不具有危害。
僵尸进程:子进程结束、父进程尚未结束,如果父进程未处理子进程的结束信息,未回收子进程的内核区数据,此时子进程为僵尸进程。在进程概述中讲到,子进程结束不会自动释放内核区数据,等待父进程接收并处理其结束信息。如果处于将死状态的子进程一直存在,将不断占用内核区内存和PID,大量积累具有危害。
父进程不处理僵尸进程,在其结束后,init进程会为其处理僵尸进程。但是如果父进程是长期运行的进程(如服务器进程),危害将持续积累。
wait函数与waitpid函数用于处理和回收子进程内核区数据。
wait 函数回收子进程
wait函数与waitpid函数用于接收进程状态变化,默认阻塞进程等待任意子进程状态变化。
返回值:当任意进程状态变化,函数返回子进程PID,设置wstatus值表示该子进程状态变化编码(后述情况不设置wstatus);当WNOHANG选项打开,并且存在未结束子进程,返回0(当WNOHANG选项关闭,并且存在未结束子进程,阻塞);当所有子进程已回收,抛出No child process错误,返回-1;当出现其他错误,返回-1。
pid:分为下述情况:
< -1:等待任意组ID为-pid(pid的绝对值)的子进程状态变化。
-1:等待任意子进程状态变化。
0:等待任意组ID与函数启动时父进程的组ID相同的子进程状态变化。
> 0:等待进程ID为pid的子进程状态变化。
如果对应进程符号ID要求但不是子进程(子孙进程不属于子进程)的,不属于控制范围。
wstatus:整型指针,用于接收状态变化编码,可为NULL表示不接收信息。解码宏如下:
WIFEXITED(wstatus):返回01整型。是否正常退出(exit为正常退出)。
WEXITSTATUS(wstatus):返回整型。如果上宏为真,返回退出状态(exit参数,0~255)。
WIFSIGNALED(wstatus):返回01整型。是否异常退出(信号退出为异常退出)。
WTERMSIG(wstatus):返回整型。如果上宏为真,返回引发退出的信号。
WIFSTOPPED(wstatus):返回01整型。是否进入暂停。
WSTOPSIG(wstatus):返回整型。如果上宏为真,返回引发暂停的信号。
WIFCONTINUED(wstatus):返回01整型。是否进程继续。
options:宏如下:(使用|连接)
(默认):等待进程正常退出、进程异常退出(以及被追踪进程暂停)。
WNOHANG:不阻塞,立即返回。
WUNTRACED:也会等待进程(未被追踪进程)被暂停。
WCONTINUED:也会等待进程被继续。
wait(wstatus)等价于waitpid(-1, wstatus, 0)。
关于更多:$ man 2 wait。
1 2 3 4 5 6 7 8 9 #include <sys/types.h> #include <sys/wait.h> pid_t wait (int *wstatus) ;pid_t waitpid (pid_t pid, int *wstatus, int options) ;#include <stdio.h> perror ("wait" );
信号回收子进程
我们注意到wait函数回收子进程时,需要主动检测是否有子进程状态变化;如果我们希望即使处理子进程,必须阻塞的回收子进程,或者使用独立线程处理子进程。在2.3.4 信号章节中将提到,子进程状态变化时将触发SIGCHLD信号,届时可以考虑使用信号即时回收子进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <sys/types.h> #include <sys/wait.h> #include <signal.h> void process_signal (int sig) { if (sig == SIGCHLD) { int ret = 1 , wstatus; while (ret > 0 ) { waitpid (-1 , &wstatus, WNOHANG); } } }