建站提交历史文章,原文写作时间 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); } } }