学习视频链接
黑马程序员-Linux系统编程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1KE411q7ee?p=129
目录
一、信号的概念
1.1 信号共性
简单、不能携带大量信息、满足条件才能发送
1.2 信号的机制
1、A 给 B 发送信号,B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似 —— 异步模式。但信号是软件层面上实现的中断,早期常被称为 “软中断”。
2、信号的特质:
由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延
迟时间非常短,不易察觉。
3、每个进程收到的所有信号,都是由内核负责发送的,内核处理。
1.3 与信号相关的事件和状态
1、产生信号:
(1) 按键产生,如:Ctrl + c、 Ctrl + z、 Ctrl + \
【Ctrl + c、Ctrl + \ 都能终止程序,但是发送的信号不同。Ctrl + z 暂停进程,通过输入 fg 指令可以让他继续执行】
(2) 系统调用产生,如:kill、 raise、 aborte
(3) 软件条件产生,如:定时器 alarm
(4) 硬件异常产生,如:非法访问内存(段错误)、除 0(浮点数例外)、内存对齐出错(总线错误)
(5) 命令产生,如:kill 命令
2、递达:
递送并且到达进程
3、未决:
产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态
4、信号的处理方式
(1) 执行默认动作
(2) 忽略(丢弃)
(3) 捕捉(调用户处理函数)
5、Linux 内核的进程控制块 PCB 是一个结构体,task struct,除了包含进程 id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集
6、阻塞信号集(信号屏蔽字):
将某些信号加入集合,对他们设置屏蔽,当屏蔽 x 信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
7、未决信号集:
(1) 信号产生,未决信号集中描述该信号的位立刻翻转为 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂
(2) 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态
8、图示
下面图中未决信号集有一个中断信号将要被执行,信号被处理后变为 0
下面图中未决信号集有一个中断信号将要被执行,但是信号屏蔽字将这个信号屏蔽了,一直处理不了
1.4 信号的编号
常规信号为前 31 个,都有默认事件和处理动作
实时信号没有默认动作
1.5 信号四要素
1、编号 2、名称 3、事件 4、默认处理动作
通过 man 7 signal 查看
kill 进程id 默认使用信号量为 15 的信号杀死进程
(1) SIGHUP:当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程
(2) SIGINT:挡用户按下了 <ctrl+c> 组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程
(3) SIGQUIT:当用户按下 <ctrI+\> 组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程
(4) SIGILL:CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件
(5) SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止里程并产生 core 文件
(6) SIGABRT:调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件
(7) SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件
(8) SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误。默认动作为终止进程并产生 core 文件
(9) SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法
(10) SIGUSR1:用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程
(11) SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件
(12) SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程
(13) SIGPIPE:Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程
(14) SIGALRM:定时器超时,超时的时间由系统调用 alarm 设置。默认动作为终止进程
(15) SIGTERM:程序结束信号,与 SIGKILL 不同的是,该信号可以被阳塞和终止。通常用来要示程序正常退出。执行 shell 命令 Kill 时,缺省产生这个信号。默认动作为终止进程
(16) SIGSTKFLT:Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程
(17) SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号
(18) SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续 / 忽略
(19) SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程
(20) SIGTSTP:停止终端交互进程的运行。按下 <ctrl+z> 组合键时发出这个信号。默认动作为暂停进程
(21) SIGTTIN:后台进程读终端控制台。默认动作为暂停进程
(22) SIGTTOU:该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程
(23) SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带处数据到达,默认动作为忽略该信号
(24) SIGXCPU:进程执行时问超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程
(25) SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程
(26) SIGVTALRM:虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间。默认动作为终止进程
(27) SGIPROF:类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间。默认动作为终止进程
(28) SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号
(29) SIGIO:此信号向进程指示发出了一个异步 IO 事件。默认动作为忽略
(30) SIGPWR:关机。默认动作为终止进程
(31) SIGSYS:无效的系统调用。默认动作为终止进程井产生 core 文件
(34) SIGRTMIN ~ (64) SIGRTMAX:LINUX 的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程
二、kill函数/命令产生信号
2.1 kill产生命令
1、kill 命令产生信号:kill -SIGKILL pid
kill -SIGKILL -1 杀死所有的进程,电脑重启
2.2 kill函数
1、kill 函数:给指定进程发送指定信号(不一定杀死)
int kll(pid_t pid, int sig); 成功:0;失败: -1 (ID 非法,信号非法,普通用户杀 init 进程等权级问题),设置 errno
sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致
pid>0:发送信号给指定的进程
pid=0:发送信号给与调用 kill 函数进程属于同一进程组的所有进程
pid<0:取 |pid| 发给对应进程组
pid=-1:发送给进程有权限发送的系统中所有进程
还可以使用其他的信号杀死进程
2.3 进程组:
每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成-一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同
2.4 权限保护:
super 用户 (root) 可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。kill -9 (root 用户的 pid) 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户 ID == 接收者实际或有效用户 ID
2.5 其他几个发送信号的函数
int raise(int sig);
void abort(void);
三、alarm 和 setitimer 函数
3.1 alarm函数
1、作用
设置定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 (14) SIGALRM 信号。进程收到该信号,默认动作终止
每个进程都有且只有唯一个定时器
2、unsigned int alarm(unsigned int seconds);
返回 0 或剩余的秒数,无失败
3、常用:
取消定时器 alarm(0),返回旧闹钟余下秒数
4、案例: alarm(5)→3sec→alarm(4) →5sec →alarm(5) →alarm(0)
调用 alarm(4) 返回 2,调用 alarm(5) 返回 0,调用 alarm(0) 返回 5
5、注意
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸... 无论进程处于何种状态,alarm 都计时
6、案例:一秒钟 for 循环的次数
(1) 基础版本
(2) 运行时候加上查看时间的内容
在用户空间(用户态)运行了 0.008 秒,再系统空间(核心态)运行了 0.873 秒
实际执行时间 = 系统时间 + 用户时间 + 等待时间
(3) 运行的内容写入文件
得出结论:优化 IO 能显著提高程序运行效率
3.2 setitimer函数
1、作用
设置定时器(闹钟)
可代替 alarm 函数。精度微秒 us,可以实现周期定时
2、int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
成功:0; 失败:-1,设置 errno
参数:
(1) which: 指定定时方式。
① 自然定时:ITIMER_REAL → (14) SIGLARM 计算自然时间(传入这个参数和上面的 alarm 函数的效果就是一样的)
② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → (26) SIGVTALRM 只计算进程占用 cpu 的时间
③ 运行时计时(用户 + 内核):ITIMER_PROF → (27) SIGPROF 计算占用 cpu 及执行系统调用的时间
(2) new_value:定时秒数
(3) old_value:传出参数,上次定时剩余时间
(4) struct itimerval 结构体
it_interval:用来设定两次定时任务之间间隔的时间
it_value:定时的时长
程序运行两秒后第一次发送时钟信号,后面每隔5秒发送一次
四、信号集操作函数
我们可以设置阻塞信号集,通过新建一个信号集,如下图
4.1 信号集操作函数
sigset_t set; 自定义信号集
sigemptyset(sigset_t *set); 清空信号集
sigfillset(sigset_t *set); 全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
sigismenber(const sigset_t *set, int signum); 判断一个信号是否在集合中。如果在集合中返回 1,否则返回 0
4.2 设置信号屏蔽字和解除屏蔽
int siaprocmas(int how, const sigset_t *set, sigset_t *oldset);
how:
SIG_BLOCK:设置阻塞
SIG_UNBLOCK:取消阻塞
SIG_SETMASK:用自定义 set 替换 mask
set:自定义 set
oldset:旧有的 mask
4.3 查看未决信号集
int sigpending(sigset_t *set);
set:传出的未决信号集
4.4 操作流程
第一步:自己新建一个阻塞信号集
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
这个阻塞集合中除了 SIGINT 为 1,其他的均为 0
第二步:用自己创建的阻塞信号集和原来的做一个或操作
int siaprocmas(SID_BLOCK, &set, &oldset);
oldset 表示把原来的阻塞信号集传递回来
第三步:查看被阻塞的未决信号集
sigpending(&myset);
myset 传出参数
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void print_set(sigset_t *set)
{
int i;
for (i = 1; i < 32; i++) {
if (sigismember(set, i)) {
putchar('1');
}
else {
putchar('0');
}
}
printf("\n");
}
int main(void) {
sigset_t set, oldset, pedset;
int ret = 0;
sigemptyset(&set);
sigaddset(&set, SIGINT);
ret = sigprocmask(SIG_BLOCK, &set, &oldset);
if (ret == -1) {
sys_err("sigprocmask error");
}
for (int k = 0; k < 60; k++) {
ret = sigpending(&pedset);
if (ret == -1) {
sys_err("sigpending error");
}
print_set(&pedset);
sleep(1);
}
return 0;
}
测试其他的
其中 9 号信号不能被屏蔽阻塞
五、信号捕捉
5.1 signal函数
1、作用:
注册一个信号捕捉函数
2、typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
3、注意:
该函数由 ANSI 定义,由于历史原因在不同版本的 Unix 和不同版本的 Linux 中可能有不同的行为。因此应该尽量避免使用它,取而代之使用 sigaction 函数
4、代码
设置了捕捉就执行捕捉动作,没有设置捕捉就执行默认动作
5.2 sigaction函数
1、作用
修改信号处理动作(通常在 Linux 用其来注册一个信号的捕捉函数)
2、int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
成功:0;失败:-1,设置 errnoe
参数:
act:传入参数,新的处理方式;oldact:传出参数,旧的处理方式
3、结构体
sa_handler 和 signal 的处理函数差不多
sa_mask 在执行信号处理函数的时候可能会有其他的信号到来,sa_mask 就可以设置阻塞哪些信号。注意仅仅是在信号处理函数正在执行时才能阻塞某些信号,如果信号处理程序执行完了,那么依然能接收到这些信号
sa_flag = 0,本信号默认屏蔽,不需要设置 mask
4、函数
5、特性
(1) 进程正常运行时, 默认PCB中有一个信号屏蔽字,假定为 ★,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由众来指定。而是用 sa_mask 来指定。调用完信号处理函数,再恢复为★
(2) XXX 信号捕捉函数执行期间,XXX 信号自动被屏蔽
(3) 阻塞的常规信号不支持排队,产生多次只记录一次。(后 32 个实时信号支持排队)
在睡眠的 10 秒内,如果传入 ctrl + \ 的信号会直接退出,如果想等执行完了睡眠 10 秒后再退出,可以按照下面的写法来写
5.3 内核实现信号捕捉过程
六、SIGCHLD信号
6.1 SIGCHLD的产生条件
子进程终止时
子进程接收到 SIGSTOP 信号停止时
子进程处在停止态,接受到 SIGCONT 后唤醒时
6.2 借助SIGCHLD信号回收子进程
1、子进程结束运行,其父进程会收到 SIGCHLD 信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收
2、代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void catch_child(int signo)
{
pid_t wpid;
wpid = wait(NULL);
if (wpid == -1) {
sys_err("wait error");
}
printf("1. catch child where id = %d\n", wpid);
return;
}
int main(void)
{
pid_t pid;
int i;
for (i = 0; i < 10; i++) {
if ((pid = fork()) == 0) {
break;
}
}
if (i == 10) {
struct sigaction act;
act.sa_handler = catch_child; // 设置回调函数
sigemptyset(&(act.sa_mask)); // 清空sa_mask屏蔽字,只在sig_catch工作时有效
act.sa_flags = 0; // 注册信号捕捉函数
sigaction(SIGCHLD, &act, NULL); // 注册信号捕捉函数
printf("2. I'm parent where pid = %d\n", getpid());
while(1);
}
else {
printf ("3. I'm child where pid = %d\n", getpid());
}
return 0;
}
有一些僵尸进程,没有被回收
正在回收第一个子进程,第二个第三个子进程也死亡,发送信号过来,信号是不排队的,就会有一个没有被回收。
3、解决上述代码的问题
修改信号处理函数,使得一次处理可以回收多个子进程
4、一些其他的改进
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void catch_child(int signo)
{
pid_t wpid;
int status;
while ((wpid = waitpid(-1, &status, 0)) != -1) {
if (WIFEXITED(status)) {
printf("1. catch child where id = %d, ret = %d\n", wpid, WEXITSTATUS(status));
}
}
return;
}
int main(void)
{
pid_t pid;
int i;
for (i = 0; i < 10; i++) {
if ((pid = fork()) == 0) {
break;
}
}
if (i == 10) {
struct sigaction act;
act.sa_handler = catch_child; // 设置回调函数
sigemptyset(&(act.sa_mask)); // 清空sa_mask屏蔽字,只在sig_catch工作时有效
act.sa_flags = 0; // 注册信号捕捉函数
sigaction(SIGCHLD, &act, NULL); // 注册信号捕捉函数
printf("2. I'm parent where pid = %d\n", getpid());
while(1);
}
else {
sleep(1); // 防止设置信号捕捉前执行完子进程
printf ("3. I'm child where pid = %d\n", getpid());
return i;
}
return 0;
}
七、中断系统调用
7.1 系统调用分类
慢速系统调用和其他系统调用
1、慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如 read、write、pause、wait ...
2、其他系统调用:getpid、getppid、fork ...
7.2 结合 pause 回顾慢速系统调用
慢速系统调用被中断的相关行为,实际上就是 pause 的行为:如 read
① 想中断 pause,信号不能被屏蔽
② 信号的处理方式必须是捕捉(默认、忽略都不可以)
③ 中断后返回 -1,设置 errmo,为 EINTR(表 “被信号中断”)可修改 sa_ flags 参数来设置被信号中断后系统调用是否重启。SA_ INTERRURT 不重启。SA_ RESTART 重启