课程目标:
构建一个基于主机系统的多客户即时通信/聊天室项目
涉及的理论知识
进程控制:僵尸进程/孤儿进程、进程控制、守护进程。。。
进程间通信:管道、命名管道、信号。。。
多线程编程: 锁、信号量。。。
参考教程
Robert Love, Linux System program
进程结构
进程由程序、数据和进程控制三部分组成
进程状态
TASK_RUNNING(运行): R 可执行状态。正在执行,在就绪队列中等待。
TASK_INTERRUPTIBLE(可中断): S 睡眠(阻塞)。如果条件满足,内核将其状态设置为运行。收到信号而被提前唤醒并投入运行。
TASK_UNINTERRUPTIBLE(不可中断): D 同可中断状态,但不会因为接收到信号而被唤醒
TASK_ZOMBIE(僵死): Z 该进程已经结束,但其父进程尚未调用wait(),子进程的进程描述符仍然被保留着。
TASK_STOPPED(停止): T 停止执行。这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
进程状态的查看
ps:显示瞬间进程的状态
常用参数:
l: 长格式输出
u: 按用户名和启动时间的顺序来显示进程
j: 用任务格式来显示进程
f: 用树形格式来显示进程
a: 显示所有用户的所有进程
x: 显示无控制终端的进程
r: 显示运行中的进程
ww: 避免详细参数被截断
$ps //列出当前shell里当前用户的进程
$ps –u yuhong //列出用户yuhong运行的所有进程
$ps –el //以详细列表方式显示运行的所有进程
$ps aux //以详细的BSD风格显示运行的所有进程
%MEM:占用的内存的使用率
VSZ : 虚拟内存大小,即一个程序完全驻留在内存的话需要占用多少内存空间
RSS: 当前实际占用了多少内存
STAT: 进程当前状态(R/S/D/Z/T)
后缀:
< (高优先级进程)
N (低优先级进程)
L (内存锁页)
s (该进程为会话首进程)
+ (前台进程)
l (多线程进程)
进程创建与终止
1、进程的创建
创建函数: pid_t fork(void); (在父进程返回,fork()返回子进程ID,在子进程中返回,fork()返回0。当进程数达到上限或者内存不足时,可能会出错,返回值为-1,系统调用并不直接返回错误码,而是将错误码放在全局变量errno中)
各种错误情况下errno的值: 1) 进程达到上限 errno=EAGAIN
2) 系统内存不足 errno=ENOMEM
查看errno数值的意义
errno.h
man 3 errno
获取进程ID:getpid(); getppid();
应该避免产生“孤儿进程”(孤儿进程还未结束,父进程却已经结束),解决方法:子进程托孤,或者让其父进程最后退出。
子进程托孤:init进程(PID=1)接管。
Questions:
如何实现子进程托孤?fork()例3中的子进程为何能够在父进程退出后,托孤给init进程(难道父进程退出后,自动托孤,不用额外的操作)?
fork()例3中为什么原进程会存在一个父进程?
子进程都继承了父进程哪些东西?试用代码举例。
2、Linux中的两个特殊的进程
0号进程:所有进程的祖先
swapper进程(调度进程):负责进程间的调度,内核直接控制,用户进程无法访问。
执行cpu_idle()函数
没有其他进程处于TASK_RUNNING,内核会选择0号进程运行
0号进程创建的1号进程
初始化进程在内核引导流程结束时被调用,用于初始化系统环境。初始化文件是/erc/rc*文件、/etc/inittab文件及/etc/init.d目录下的文件。初始化进程从不退出。
init进程创建和监控其他进程的活动
接管孤儿进程
3、进程的终止
1)显式的系统调用
#include <stdlib.h> void exit(int status); //退出前把文件缓冲区的内容写回文件 #include <unistd.h> void _exit(int status); //退出后缓冲区数据丢失
这两个函数调用后,进程转化为僵尸进程。
2)从程序结尾离开
3)被信号终止 SIGTERM(signal terminate) SIGKILL
kill [-s <信号名称或编号>][程序]
kill [-l <信号编号>]
若不加<信息编号>选项,则-l参数会列出全部的信息名称。
//强行中止(杀掉)一个进程pid为324的进程:
#kill -9 324
#free
Questions:
进程管理中信号有哪些,以及编号都是什么,如何使用?
4)被内核杀掉 Segmentation violation
当进程出现异常时,会被内核杀掉。
进程终止内核会传送一个SIGCHLD(signal child)信号给它的父进程
若一个子进程在终止时整体消失,父进程将无法取回任何的信息
若子进程先于它的父进程结束,则内核应该让子进程进入僵尸进程的状态,等待父进程来打听它的状态,状态打听后,僵尸进程才会正式结束。
僵尸进程的内核数据结构
僵尸进程只会保留最小的骨架:进程的PID,退出状态,运行时间
僵尸进程的避免:
i 父进程通过wait和waitpid等函数等待子进程结束(导致父进程立刻阻塞自己,直到有一个子进程退出)。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); wait(&status) =>waitpid(-1,&status,0)
返回值:1.结束的子进程pid 2.-1,如果没有子进程
status(两个字节):1.高字节:子进程exit时设置的代码,低字节为0 2.如果子进程的退出是因为收到信号,低字节为信号的编码
有时会见到wait函数的参数是NULL,表示父进程并不关心子进程的状态,只是等待子进程结束,并获得子进程信息,防止其成为孤儿进程或僵死进程。
pid_t waitpid(pid_t pid, int *status, int options);
pid取值:
①< -1: 等待进程组id为pid的子进程的结束
② -1: 等待任意子进程的结束(任意一个)
③ 0: 等待进程组id跟父进程进程组id相同的子进程的结束
④ >0:等待进程id为pid的子进程的结束
Options可以是以下几个常数中的一个或多个
①WNOHANG: 如果没有子进程退出的话马上返回
②WUNTRACED:如果有子进程停止的话返回
③WCONTINUED:如果一个停止的子进程重新开始执行的话返回(发送SIGCONT)
ii 如果父进程很忙,可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。
signal系统调用详解:
功能描述:为指定的信号安装新的处理句柄。信号处理句柄可能是用户指定的函 数,SIG_IGN 或 SIG_DFL。当信号到达时,如果其处理句柄是SIG_DFL,那么会以默认的方式处理信号;如果其处理句柄是SIG_IGN,那么信号会被忽略;最 后,如果处理句柄是用户指定的函数,此时先将信号处理方式重置为SIG_DFL,接着有可能阻塞处理中的信号,最后是调用信号处理句柄。
用法:
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
参数:
signum:信号编码。
handler:新的信号处理句柄。
返回说明:
成功执行时,返回以前的信号处理句柄。失败返回SIG_ERR。
iii 如果父进程不关心进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)通知内核,内核会回收,并不再给父进程发送信号。
iv Stevens的两次fork避免僵尸进程:就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。
状态标志:
信号:
三种方式执行多任务处理:轮询、中断、DMA(与中断的区别)
Questions:
为什么两次fork可以将孙进程托孤给init进程?
handler句柄是什么东西?
信号处理句柄可能是用户指定的函 数,SIG_IGN 或 SIG_DFL。
4、进程组
一个或多个进程的集合
作业控制
getpgrp() & setpgid()
To be continued...