最近在学习 APUE,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习APUE的童鞋们提供一份参考。
本系列博文均根据学习《UNIX环境高级编程》一书总结而来;
运行环境:
- 操作系统: ubutnu 16.04
- 编译器:QtCreator CLion 2020.3
信号
信号是软件中断。很多重要的应用程序都需要处理信号,信号提供了一种处理异步事件的方法;
信号机制
信号的状态
信号有三种状态:产生、未决和递达。
信号的产生
Ø 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
Ø 系统调用产生,如:kill、raise、abort
Ø 软件条件产生,如:定时器alarm
Ø 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
Ø 命令产生,如:kill命令
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
递达:递送并且到达进程。
信号的处理
执行默认动作
忽略信号(丢弃不处理)
捕捉信号(调用用户的自定义的处理函数)
信号的特质
信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。
Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
信号四要素
通过man 7 signal可以查看信号相关信息
1 信号的编号
Ø 使用kill -l命令可以查看当前系统有哪些信号,不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。
信号的名称
产生信号的事件
信号的默认处理动作
Ø Term:终止进程
Ø Ign:忽略信号 (默认即时对该种信号忽略操作)
Ø Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
Ø Stop:停止(暂停)进程
Ø Cont:继续运行进程
特别需要注意的是:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
几个常用到的信号
SIGINT、SIGQUIT、SIGKILL、SIGSEGV、SIGUSR1、SIGUSR2、SIGPIPE、SIGALRM、SIGTERM、SIGCHLD、SIGSTOP、SIGCONT
信号函数
signal
函数作用:注册信号捕捉函数;
函数原型
1 |
|
函数参数:
signum:信号编号
handler:信号处理函数
1 | /** |
kill
给指定进程发送指定信号
kill命令:kill -SIGKILL 进程PID
kill函数原型:int kill(pid_t pid, int sig);
函数返回值:
Ø 成功:0;
Ø 失败:-1,设置errno
函数参数:
Ø sig信号参数:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
Ø pid参数:
pid > 0: 发送信号给指定的进程。
pid = 0: 发送信号给与调用kill函数进程属于同一进程组的所有进程。
pid < -1: 取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
1 | /** |
raise
raise函数
Ø 函说描述:给当前进程发送指定信号(自己给自己发)
函数原型:int raise(int sig);
函数返回值:成功:0,失败非0值
函数拓展:raise(signo) == kill(getpid(), signo);
abort
函数描述:给自己发送异常终止信号 6) SIGABRT,并产生core文件
函数原型:void abort(void);
函数拓展:abort() == kill(getpid(), SIGABRT);
alarm
1 | unsigned int alarm(unsigned int seconds); |
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14**)**SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
1 | /** |
setitimer
1 | int setitimer(int which, const struct itimerval *new_value, |
设置定时器(闹钟),可代替alarm函数,精度微秒us,可以实现周期定时。
which:指定定时方式
自然定时:ITIMER_REAL → 14**)**SIGALRM计算自然时间
虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26**)**SIGVTALRM 只计算进程占用cpu的时间
运行时计时(用户+内核):ITIMER_PROF → 27**)**SIGPROF计算占用cpu及执行系统调用的时间
new_value:struct itimerval, 负责设定timeout时间。
itimerval.it_value: 设定第一次执行function所延迟的秒数 itimerval.it_interval: 设定以后每几秒执行function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
- old_value: 存放旧的timeout值,一般指定为NULL
1 |
|
信号集相关
未决信号集和阻塞信号集的关系
阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于未决状态的信号的集合,这两个集合存储在内核的PCB中。
- 下面以SIGINT为例说明信号未决信号集和阻塞信号集的关系:
当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:
如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集上该位置上的值保持为1,表示该信号处于未决状态;
如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。
信号集相关函数
由于信号集属于内核的一块区域,用户不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集的相关操作。
信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集进行添加、删除等操作。
- int sigemptyset(sigset_t *set);
函数说明:将某个信号集清0
函数返回值:成功:0;失败:-1,设置errno
- int sigfillset(sigset_t *set);
函数说明:将某个信号集置1
函数返回值:成功:0;失败:-1,设置errno
- int sigaddset(sigset_t *set, int signum);
函数说明:将某个信号加入信号集合中
函数返回值:成功:0;失败:-1,设置errno
- int sigdelset(sigset_t *set, int signum);
函数说明:将某信号从信号清出信号集
函数返回值:成功:0;失败:-1,设置errno
- int sigismember(const sigset_t *set, int signum);
函数说明:判断某个信号是否在信号集中
函数返回值:在:1;不在:0;出错:-1,设置errno
- sigprocmask函数
函数说明:用来屏蔽信号、解除屏蔽也使用该函数。其本质,读
取或修改进程控制块中的信号屏蔽字(阻塞信号集)。
特别注意,屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数返回值:成功:0;失败:-1,设置errno
函数参数:
u how参数取值:假设当前的信号屏蔽字为mask
l SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
l SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
l SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
u set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。
u oldset:传出参数,保存旧的信号屏蔽字。
- sigpending函数
² 函数原型:int sigpending(sigset_t *set);
² 函数说明:读取当前进程的未决信号集
² 函数参数:set传出参数
² 函数返回值:成功:0;失败:-1,设置errno
练习:编写程序,设置阻塞信号集并把所有常规信号的未决状态打印至屏幕。
信号捕捉
sigaction
1 | int sigaction(int signum, const struct sigaction *act, |
sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
1 |
|
sigchld
使用信号父进程回收子进程
1 | /** |