进程管理 - d10¶
回顾:
进程控制
在Unix/Linux系统中,查看进程的命令:
ps - 只能查看当前终端启动的进程
ps -aux Linux专用的,Unix系统不直接支持
ps -ef 通用的
某些Unix系统有两个ps命令,可以用whereis 查看一下。
kill -9 进程的PID 杀死对应的进程
其实就是发送9信号给对应进程
如何创建子进程?
fork() - 通过复制 父进程创建子进程
vfork() + execl() - 不复制父进程的任何东西,创建子进程,子进程会执行完全独立的代码(与父进程无关)
fork() 会复制除了代码区之外的所有内存区域,代码区和父进程共享。如果父进程有文件描述符,子进程会复制描述符,但不复制文件表(父子进程使用相同的文件表)。子进程也会复制父进程的输入输出缓冲区。
今天:
wait()和waitpid() 让父进程等待子进程的结束进程结束的方式
vfork()和execl()
信号
进程的结束方式
正常结束
1 在main()函数中,执行了return语句(没有写的,系统会在最后自动加上return语句,返回内存中的任意值)
2 执行exit(int)函数(任意函数中都可以),exit()中的参数是退出状态码,一般是用非负数代表功能已经完成,用负数代表功能没有完成
3 _exit() 或 _Exit() 也能退出进程
4 最后一个线程正常结束
非正常结束
1 被信号终止
2 最后一个线程被其他线程取消
exit() 、 _Exit()和_exit()的区别
1 _Exit()和_exit()几乎没有区别,_Exit()是标C函数,另外一个是Unix C函数。
2 _Exit()是立即退出,不会有多余的时间。
3 exit()不是立即退出,甚至可以执行另外一个函数后再退出。另外的函数可以用atexit()注册。
函数wait()和waitpid()可以让父进程等待子进程的结束,从而取到子进程的退出状态码(exit()或return)。
wait()函数等待任意一个子进程的结束,没法指定等待哪个子进程;也必须以阻塞的方式等待。
waitpid()更加灵活,可以指定等待哪个/哪些 子进程,甚至可以设置以 非阻塞的方式等待(可以不等)。
pid_t waitpid(pid_t pid , int* status, int options)
函数功能:让父进程等待子进程的结束,并取得退出码。
参数: pid就是父进程等待哪个/哪些 子进程
>0 - 等待 进程ID 等于 pid的那个子进程
-1 - 等待任意一个子进程
0 - 等待本组子进程(和父进程同一个进程组的)
<-1 - 等待进程组id=-pid的子进程
status 可以取得结束子进程的退出情况和退出状态码,需要借助宏函数实现数据的筛选(WIFEXITED() 和 WEXITSTATUS())
options 可以设置是否等待,0 代表等待,WNOHANG 不等待。
返回: 大于0 就是 结束子进程的PID
0 就是WNOHANG时 没有子进程结束直接返回 0
-1 代表出错。
注: wait()和waitpid() 都可以回收僵尸子进程的资源。(殓尸工)
vfork() - 从语法上说,vfork()和fork() 没有任何的区别。区别在于vfork() 不会复制 父进程的任何资源,子进程会直接占用父进程的资源,
父进程被阻塞(暂停运行), 只有在以下两种情况下父进程才会解除阻塞继续运行:
1 子进程运行结束,把资源还给父进程,父进程继续运行。
2 子进程执行了 exec 系列函数(比如execl()),也会把资源还给父进程,子进程将加载 execl() 所指定的进程资源。
其中,情况1 是没有现实意义。
fork() 之后 父子进程 谁先运行不一定,vfork() 之后一定是子进程先运行(父进程没资源)。
注: vfork()创建的子进程,如果没有execl()或execl()失败,一定要用exit() 函数 显式退出。否则可能导致出错或者死循环。
vfork()+execl() 创建和执行子进程,组合方式如下:
vfork() 负责启动新的子进程,但不负责提供子进程的代码和数据。
execl() 无法启动新进程,但可以提供新的代码和数据 供当前进程更新。
int execl(char* path, char* cmd, ...)
函数功能: 启动一个全新的程序,替换当前的进程
参数: path 就是 新程序在硬盘对应的文件路径
cmd 就是 执行程序的命令
... 可以包括 选项、参数,以NULL 作为 结束。
execl("/bin/ls","ls","-l",NULL) 调用系统的ls
返回: 成功就 执行全新的代码,因此 无返回。失败返回 -1 。
失败最可能的情况就是 第一个参数写错了。
练习: vfork()+execl()的用法
用vfork()+execl()方式创建子进程,其中子进程实现的功能是:
在 execl()之前打印子进程 PID,然后在子进程中启动新的程序,新的程序功能 也是打印 当前进程的PID (这两个PID应该是一样的)。
提示:需要写两个程序,连接成两个不同的名字。
重点在于 如何创建子进程,fork() 和 vfork()+execl() 。
信号(signal)
在Unix系统中,信号是最常见的 软件中断方式之一。大多数的 软件中断都借助信号实现。
中断 是中止当前正在执行的程序,转而执行其他的代码。中断分为硬件中断和软件中断,一般可控的 都是采用软件中断。有些硬件的故障也会转换成软件中断进行处理,一般都是信号。
信号 本质是非负整数,Unix系统 信号是 0-48,Linux系统是0-64,中间不保证连续。其中,信号0 有特殊的用途,没有 实际的意义。真正使用时,信号 从1 开始。
命令 kill 就是用来处理信号的命令,kill 可以给进程发信号。
$ kill -l 可以查看 都有哪些信号。
每个信号除了值,都还有一个宏名称,比如:信号2 叫SIGINT,
所有的宏名称都以 SIG 开头。信号分为两类,可靠信号和不可靠信号。
不可靠信号 就是 1-31 ,特点是 不支持排队,因此多次发动时可能丢失。
可靠信号 就是 34-64 ,特点是 支持排队,因此不会丢失。
使用信号编程时,用宏名称有更好的通用性。
信号的处理方式 (默认、忽略和自定义) - 重点
1 当信号到来时,如果不做任何的设置,将采用默认处理。默认处理一般 就是退出进程。
2 忽略处理,当该信号没有来过,不做任何的处理。
3 自定义处理,程序员按照自己的方式,写函数处理信号。(必须会)
注: 不是所有的信号都能忽略和自定义的,信号9 只能采用默认处理
每个用户只能给自己的进程发信号,其他用户的进程无权限发送信号。但root 用户可以给所有的用户进程发信号。
Unix系统/Linux系统提供了设置信号处理方式的函数:
signal() - 功能简单,但实用主讲
sigaction() - 功能复杂,有些功能用不到
void (*fa) (int) signal(int signum, void (*fa)(int) )
函数功能:指定某个信号的处理方式
参数: signum 就是 哪个信号,一般用宏名称
第二个参数是一个函数指针,支持三个值:
1 SIG_IGN 处理方式为 忽略
2 SIG_DFL 处理方式为 默认
3 自定义一个函数,把函数名放在这里 (自定义处理)
返回: 正常会返回之前的信号处理方式,失败返回 SIG_ERR。