当前位置: 首页 > news >正文

Linux系统编程—Linux进程信号

第一章:信号快速认识

1-1 生活角度的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
  • 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

基本结论:

  • 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
  • 信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
  • 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。
  • 信号到来 | 信号保存 | 信号处理
  • 怎么进行信号处理啊?a.默认 b.忽略 c.自定义, 后续都叫做信号捕捉。

1-2 技术应用角度的信号

1-2-1 一个样例

// sig.cc
#include <iostream>
#include <unistd.h>
int main() {while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ g++ sig.cc - o sig
$ . / sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C
  • 用户输入命令,在Shell下启动一个前台进程。
  • 用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
  • 前台进程因为收到信号,进而引起进程退出

1-2-2 一个系统函数

NAMEsignal - ANSI C signal handling
SYNOPSIS#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
参数说明:signum:信号编号[后⾯解释,只需要知道是数字即可]handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法

而其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明一下,这里需要引入一个系统调用函数

开始测试

#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber <<std::endl;
}
int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT/*2*/, handler);while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ g++ sig.cc - o sig
$ . / sig
我是进程 : 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C我是 : 212569, 我获得了⼀个信号 : 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C我是 : 212569, 我获得了⼀个信号 : 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!

思考:

  • 这里进程为什么不退出?
  • 这个例子能说明哪些问题?信号处理,是自己处理
  • 请将生活例子和 Ctrl-C 信号处理过程相结合,解释一下信号处理过程?进程就是你,操作系统就是快递员,信号就是快递,发信号的过程就类似给你打电话

注意:

  • 要注意的是,signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!!
  • Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  • Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C这种控制键产生的信号。
  • 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
  • 关于进程间关系,我们在网络部分会专门来讲,现在就了解即可。
  • 可以渗透 & 和 nohup

1-3 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

1-3-1 查看信号

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal

1-3-2 信号处理

( sigaction 函数稍后详细介绍),可选的处理动作有以下三种:

  • 忽略此信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber<< std::endl;
}
int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ g++ sig.cc - o sig
$ . / sig
我是进程 : 212681
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C^ C^ C^ C^ C^ CI am a process, I am waiting signal! // 输⼊ctrl+c毫⽆反应
I am a process, I am waiting signal!

  • 执行该信号的默认处理动作。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber<< std::endl;
}
int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT/*2*/, SIG_DFL);while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ g++ sig.cc - o sig
$ . / sig
我是进程 : 212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C // 输⼊ctrl+c,进程退出,就是默认动作

  • 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自定义捕捉(Catch)一个信号。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber <<std::endl;
}
int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT/*2*/, handler);while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ g++ sig.cc - o sig
$ . / sig
我是进程 : 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C我是 : 212569, 我获得了⼀个信号 : 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C我是 : 212569, 我获得了⼀个信号 : 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!

课堂示例

//演示捕获信号并执行自定义动作
//int:收到了哪一个信号
void myhandler(int signo) {cout << "process get a signal: " << signo << endl;
}
int main() {//方式一signal(SIGINT, myhandler);//只需要设置一次,往后都有效//把1-31号信号全部捕捉且设为自定义动作(总之不退出)。是否进程就无法退出了,不是。//信号的产生和代码的运行是异步的//方式二signal(3, myhandler);//普通信号全捕获for (int i = 1; i <= 31; i++) signal(i, myhandler);// 9和19不能别捕捉。一个是结束进程;一个是暂定进程while (true) {cout << "I am a crazy process: " << getpid() << endl;sleep(1);}return 0;
}

注意看源码:

#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);
// 其实SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型

上面的所有内容,还没有做非常多的解释,主要是先用起来,然后渗透部分概念和共识,下面从理论和实操两个层面,来对信号进行详细的学习、论证和理解。为了保证条理,采用如下思路来进行阐述:

第二章:产生信号

2-1 通过终端按键产生信号

2-1-1 基本操作

  • Ctrl+C (SIGINT) 已经验证过,这里不再重复
  • Ctrl+\(SIGQUIT)可以发送终止信号并生成core dump文件,用于事后调试(后面详谈)
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber <<std::endl;
}
int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGQUIT/*3*/, handler);//第13行while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ g++ sig.cc - o sig
$ . / sig
我是进程 : 213056
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ \我是 : 213056, 我获得了⼀个信号 : 3// 注释掉13⾏代码
$ . / sig
我是进程 : 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ \Quit

  • Ctrl+Z(SIGTSTP)可以发送停止信号,将当前前台进程挂起到后台等
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber) {std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber <<std::endl;
}
int main() {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGTSTP/*20*/, handler);//第13行while (true) {std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ . / sig
我是进程 : 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ Z我是 : 213552, 我获得了⼀个信号 : 20// 注释掉13⾏代码
$ . / sig
我是进程 : 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ Z
[1] + Stopped . / sig
whb@bite:~/ code / test$ jobs
[1] + Stopped . / sig

2-1-2 理解OS如何得知键盘有数据

OS怎么知道键盘上有数据1

OS怎么知道键盘上有数据2

2-1-3 初步理解信号起源

注意:

  • 信号其实是从纯软件角度,模拟硬件中断的行为
  • 只不过硬件中断是发给CPU,而信号是发给进程
  • 两者有相似性,但是层级不同,这点后面的感觉会更加明显

2-2 调用系统命令向进程发信号

示例代码

#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {while (true) {sleep(1);}
}$ g++ sig.cc - o sig // step 1
$ ./sig & // step 2
$ ps ajx | head - 1 && ps ajx | grep sig // step 3
PPID   PID	  PGID   SID    TTY   TPGID  STAT UID  TIME COMMAND
211805 213784 213784 211805 pts/0 213792 S    1002 0:00 ./sig

首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。

$ kill -SIGSEGV 213784
$ // 多按⼀次回⻋
[1]+ Segmentation fault ./sig
  • 213784 是 sig 进程的pid。之所以要再次回车才显示 Segmentation fault,是因为在213784 进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell 不希望Segmentation fault 信息和用户的输入交错在一起,所以等用户输入命令之后才显示。
  • 指定发送某种信号的 kill 命令可以有多种写法,上面的命令还可以写成 kill -11 213784,11 是信号 SIGSEGV 的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。

2-3 使用函数产生信号

2-3-1 kill

kill 命令是调用 kill 函数实现的。 kill 函数可以给一个指定的进程发送指定的信号。

NAMEkill - send signal to a process
SYNOPSIS#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);
RETURN VALUEOn success(at least one signal was sent), zero is returned.On error,-1 is returned, and errno is set appropriately.

样例:实现自己的 kill 命令

//演示 kill函数
void Usage(string proc) {cout << "Usage:\n\t" << proc << " signum pid\n\n";
}
//mykill signum pid
int main(int argc, char* argv[]) {if (argc != 3) {Usage(argv[0]);exit(1);}int signum = stoi(argv[1]);pid_t pid = stoi(argv[2]);int n = kill(pid, signum);if (n == -1) {perror("kill");exit(2);}return 0;
}

2-3-2 raise

raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。

NAMEraise - send a signal to the caller
SYNOPSIS#include <signal.h>int raise(int sig);
RETURN VALUEraise() returns 0 on success, and nonzero for failure.

样例:

//演示 raise
void myhandler(int signo) {cout << "process get a signal: " << signo << endl;
}
int main(int argc, char* argv[]) {signal(2, myhandler);int cnt = 0;while (true) {cout << "I am a process, pid: " << getpid() << endl;sleep(1);cnt++;if (cnt % 2 == 0) raise(2);//每2次 发1次信号//kill(getpid(), 2);//等效上方}return 0;
}
$ g++ raise.cc -o raise
$ ./raise
process get a signal: 2
process get a signal: 2
process get a signal: 2

2-3-3 abort

abort 函数使当前进程接收到信号而异常终止。

NAMEabort - cause abnormal process termination
SYNOPSIS#include <stdlib.h>void abort(void);
RETURN VALUEThe abort() function never returns.// 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。
void myhandler(int signo) {cout << "process get a signal: " << signo << endl;
}
int main(int argc, char* argv[]) {signal(SIGABRT, myhandler);int cnt = 0;while (true) {cout << "I am a process, pid: " << getpid() << endl;sleep(1);cnt++;if (cnt % 2 == 0) //在命令行中使用 kill -6 pid不能终止进程// abort();//给自己发送6号信号,并终止进程kill(getpid(), 6);//类似上方,但进程不会退出}return 0;
}

2-4 由软件条件产生信号

SIGPIPE 是一种由软件条件产生的信号,在"管道"中已经介绍过了。本节主要介绍 alarm 函数和 SIGALRM 信号。

NAMEalarm - set an alarm clock for delivery of a signal
SYNOPSIS#include <unistd.h>unsigned int alarm(unsigned int seconds);
RETURN VALUEalarm() returns the number of seconds remaining until any previouslyscheduled alarm was due to be delivered, or zero if there was no previously scheduled alarm.
  • 调用 alarm 函数可以设定一个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终止当前进程。
  • 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,"以前设定的闹钟时间还余下的时间"就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

2-4-1 基本alarm验证-体会IO效率问题

程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。
必要的时候,对SIGALRM信号进行捕捉。

// IO 多
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {int count = 0;alarm(1);while (true) {std::cout << "count : "<< count << std::endl;count++;}return 0;
}... ...
count : 107148
count : 107149
Alarm clock
// IO 少
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber) {std::cout << "count : " <<count << std::endl;exit(0);
}
int main() {signal(SIGALRM, handler);alarm(1);while (true) {count++;}return 0;
}$ g++ alarm.cc - o alarm
whb@bite:~/code/test$ ./alarm
count : 492333713

结论:

  • 闹钟会响一次,默认终止进程
  • 有IO效率低

2-4-2 设置重复闹钟

演示闹钟1

// 第一次 alarm(5) 启动定时。
// 5 秒后触发 SIGALRM → handler 被调用。
// handler 内再次 alarm(5)。
// 形成一个每隔 5 秒重复的信号循环。
void handler(int signo) {cout << "...get a sig, number: " << signo << endl;alarm(5);//在这里再添加一个闹钟,每隔5秒响一次
}
int main() {signal(SIGALRM, handler);//只打印了一次。闹钟只设置一次,只响一次,不是异常int n = alarm(5);while (1) {cout << "proc is running..." << endl;sleep(1);}return 0;
}

演示闹钟2

//主业代码(即循环打印)一直运行,每隔5秒运行work()
void work() { cout << "print log..." << endl; }
void handler(int signo) {work();alarm(5);//在这里再添加一个闹钟,每隔5秒响一次
}
int main() {signal(SIGALRM, handler);//只打印了一次。闹钟只设置一次,只响一次,不是异常int n = alarm(5);while (1) {cout << "proc is running..." << endl;sleep(1);}return 0;
}

演示闹钟3 返回值

// 假设:程序运行了 10 秒,执行了 kill -14 <pid>。
// 此时:原来的 alarm(50) 还剩 40 秒。进入 handler()。
// int n = alarm(5); 这行代码做了两件事:
// 设置了一个新的 5 秒闹钟。返回了旧闹钟的剩余时间(约 40 秒)。
void handler(int signo) {cout << "...get a sig, number: " << signo << endl;int n = alarm(5);//alarm() 的行为是 “设置新的闹钟,同时返回旧闹钟剩余的时间”。cout << "剩余时间: " << n << endl;
}
int main() {signal(SIGALRM, handler);int n = alarm(50);while (1) {cout << "proc is running..., pid: " << getpid() << endl;sleep(1);}return 0;
}

结论:

  • 闹钟设置一次,起效一次
  • 重复设置的方法
  • 如果时间允许,可以测试一下 alarm(0)

2-4-3 如何理解软件条件

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。

2-4-4 如何简单快速理解系统闹钟

系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。

现代Linux是提供了定时功能的,定时器也要被管理:先描述,再组织。内核中的定时器数据结构是:

struct timer_list {struct list_head entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct tvec_t_base_s* base;
};

我们不在这部分进行深究,为了理解它,我们可以看到:定时器超时时间expires和处理方法function。

操作系统管理定时器,采用的是时间轮的做法,但是我们为了简单理解,可以把它组织成为"堆结构"。

2-5 硬件异常产生信号

硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

2-5-1 模拟除0

//信号为什么一直触发?
void handler(int signo) {cout << "...get a sig, number: " << signo << endl;//什么都没做,只是打印消息
}
int main() {signal(SIGFPE, handler);//一直捕获信号,进程不退出cout << "div before" << endl;sleep(1);int a = 10;a /= 0;cout << "div after" << endl;sleep(1);return 0;
}

OS怎么检测到除0

2-5-2 模拟野指针

//演示野指针错误
void handler(int signo) {cout << "...get a sig, number: " << signo << endl;
}
int main() {signal(SIGSEGV, handler);//一直捕获信号,进程不退出cout << "point error before" << endl;sleep(1);int* p = nullptr;*p = 100;//野指针cout << "point error after" << endl;sleep(1);return 0;
}

OS怎么检测到野指针

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

注意:
通过上面的实验,我们可能发现:
发现一直有8号信号产生被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS 会检测是否存在异常状态,
有异常存在就会调用对应的异常处理方法。

除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的不断发出异常信号的现象。访问非法内存其实也是如此,大家可以自行实验。

2-5-3 子进程退出core dump

演示core dump1

//展示不同的core dump
int main() {pid_t id = fork();if (id == 0) {int cnt = 500;while (cnt) {cout << "I am a child process, pid:" << getpid() << " cnt:" << cnt << endl;sleep(1);cnt--;}exit(0);}int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid == id) {cout << "child quit info, rid:" << rid << " exit code:" << ((status>>8)&0xFF)<< " exit signal:" << ((status)&0x7F) << " core dump:" << ((status>>7)&1) << endl;}return 0;
}

演示cor.pid文件

int main() {int a = 10;int b = 0;a /= b;cout << "a = " << a << endl;return 0;
}

2-5-4 Core Dump

  • SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
  • 首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
  • 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做 Post-mortem Debug (事后调试)。
  • 一个进程允许产生多大的 core 文件取决于进程的 Resource Limit (这个信息保存在PCB中)。默认是不允许产生 core 文件的,因为 core 文件中可能包含用户密码等敏感信息,不安全。
  • 在开发调试阶段可以用 ulimit 命令改变这个限制,允许产生 core 文件。 首先用 ulimit 命令改变 Shell 进程的 Resource Limit ,如允许 core 文件最大为 1024K: $ ulimit -c 1024

为什么云服务器默认关闭core_dump

2-6 总结思考一下

  • 上面所说的所有信号产生,最终都要有OS来执行,为什么?OS是进程的管理者
  • 信号的处理是否是立即处理的?在合适的时候
  • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
  • 一个进程在没有收到信号的时候,能否知道,自己应该对合法信号作何处理呢?
  • 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

第三章:保存信号

当前阶段

3-1 信号其他相关常见概念

实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

3-2 在内核中的表示

信号在内核中的表示示意图

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

// 内核结构 2.6.18
struct task_struct {.../* signal handlers */struct sighand_struct* sighand;sigset_t blockedstruct sigpending pending;...
}struct sighand_struct {atomic_t count;struct k_sigaction action[_NSIG]; // #define _NSIG 64spinlock_t siglock;
};struct __new_sigaction {__sighandler_t sa_handler;unsigned long sa_flags;void (*sa_restorer)(void); /* Not used by Linux/SPARC */__new_sigset_t sa_mask;
};struct k_sigaction {struct __new_sigaction sa;void __user* ka_restorer;
};/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);struct sigpending {struct list_head list;sigset_t signal;
};

3-3 sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字,这里的"屏蔽"应该理解为阻塞而不是忽略。

3-4 信号集操作函数

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

3-4-1 sigprocmask

//代码块
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
返回值:若成功则为0, 若出错则为 - 1

调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。


如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

3-4-2 sigpending

#include <signal.h>
int sigpending(sigset_t* set);
读取当前进程的未决信号集, 通过set参数传出。
调⽤成功则返回0, 出错则返回 - 1

下面用刚学的几个函数做个实验。程序如下:

验证SIG_DFL SIG_IGN

int main() {// signal(2, SIG_IGN);//忽略2号信号signal(2, SIG_DFL);//使用2号信号默认处理方法while (1) {cout << "hello signal" << endl;sleep(1);}return 0;
}

对2号新号进行自定义捕捉

void PrintPending(sigset_t& pending) {for (int signo = 31; signo >= 1; signo--) {if (sigismember(&pending, signo)) cout << "1";else cout << "0";}cout << endl << endl;
}void handler(int signo) { cout << "catch a signo: " << signo << endl; }int main() {//0.对2号新号进行自定义捕捉signal(2, handler);//1.先对2号信号屏蔽 --- 数据预备//sigset_t:位图(bitmask)结构,主要用于 block 和 pending 信号集。sigset_t bset, oset;//在用户栈上开辟的空间,属于用户区 sigemptyset(&bset);sigemptyset(&oset);sigaddset(&bset, 2);//还未屏蔽,只是设置了信号集。//1.1 调用系统接口,将数据设置进内核sigprocmask(SIG_SETMASK, &bset, &oset);//已经屏蔽2号信号//2.重复打印当前进程的pendingsigset_t pending;int cnt = 0;while (true) {//2.1 获取int n = sigpending(&pending);if (n < 0) continue;//2.2 打印PrintPending(pending);sleep(1);//2.3 解除阻塞cnt++;if (cnt == 10) {cout << "unblock 2 signo" << endl;sigprocmask(SIG_SETMASK, &oset, nullptr);}}return 0;
}$ ./run
curr process[448336]pending: 0000000000000000000000000000000
curr process[448336]pending: 0000000000000000000000000000000
^Ccurr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

将所有信号屏蔽,信号就不会被处理了吗?肯定不行 9 19

第四章:捕捉信号

当前阶段

信号捕获的目的

什么时候被处理的

地址空间3

 操作系统内核运行机制解析

操作系统时钟中断原理

时钟中断驱动OS调度

4-1 信号捕捉的流程

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

  • 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。
  • 当前正在执行 main 函数,这时发生中断或异常切换到内核态。
  • 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。
  • 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数, sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
  • sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。
  • 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

4-2 sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以再了解一下。

 问题1:pending位图,什么时候从1->0 在执行信号捕获方法之前就清0了

void PrintPending() {sigset_t set;sigpending(&set);for (int signo = 1; signo <= 31; signo++) {if (sigismember(&set, signo)) cout << "1";else cout << "0";}cout << endl;
}void handler(int signo) {PrintPending();//在这打印pending位图,如果2号信号是0,说明处理信号前就置0了cout << "catch a signal, signal number: " << signo << endl;
}int main() {struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(act));act.sa_handler = handler;sigaction(2, &act, &oact);while (true) {cout << "I am a process: " << getpid() << endl;sleep(1);}return 0;
}

问题2:信号被处理的时候,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用

//当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,
//这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
//原因:此时的pending位图已经置为0了,且信号处理函数中可能会因为系统调用陷入内核,这样信号检测和处理的时机就出现了,
//如果不对该信号进行屏蔽且持续发该信号那么就无限递归
void PrintPending() {sigset_t set;sigpending(&set);for (int signo = 1; signo <= 31; signo++) {if (sigismember(&set, signo)) cout << "1";else cout << "0";}cout << endl;
}void handler(int signo) {cout << "catch a signal, signal number: " << signo << endl;//死循环即不返回,永远处理2号信号。此时pending表为0。//此时发送2号信号,因为2号被屏蔽,所以pending表由0->1//1是获取到该信号,且还没有被递达。(被处理前会被置为0)while (true) {PrintPending();sleep(1);}
}int main() {struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(act));act.sa_handler = handler;sigaction(2, &act, &oact);while (true) {cout << "I am a process: " << getpid() << endl;sleep(1);}return 0;
}

sigaddset

//如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,
//则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
void PrintPending() {sigset_t set;sigpending(&set);for (int signo = 1; signo <= 31; signo++) {if (sigismember(&set, signo)) cout << "1";else cout << "0";}cout << endl;
}void handler(int signo) {cout << "catch a signal, signal number: " << signo << endl;while (true) {PrintPending();sleep(1);}
}int main() {struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(act));//2号信号处理时会自动屏蔽2号,sigaddset可以在这时屏蔽更多信号sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 1);sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);act.sa_handler = handler;sigaction(2, &act, &oact);while (true) {cout << "I am a process: " << getpid() << endl;sleep(1);}return 0;
}

4-3 穿插话题 - 操作系统是怎么运行的

4-3-1 硬件中断

  • 中断向量表是操作系统的一部分,启动时就加载到内存中。
  • 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询。
  • 由外部设备触发的,中断系统运行流程,叫做硬件中断。

4-3-2 时钟中断

问题:

  • 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
  • 外部设备可以触发硬件中断,但是这是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

这样,操作系统不就在硬件的推动下,自动调度了么

4-3-3 死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。操作系统的本质:就是一个死循环!

  • 这样,操作系统就可以在硬件时钟的推动下,自动调度了。
  • 所以,什么是时间片?CPU为什么会有主频?为什么主频越快,CPU越快?主频可以作为OS调度执行速度的参考之一

4-3-4 软中断

  • 上述外部硬件中断,需要硬件设备触发。
  • 有没有可能,因为软件原因,也触发上面的逻辑?有!
  • 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑。

所以:

问题:

  • 用户层怎么把系统调用号给操作系统? - 寄存器(比如EAX)
  • 操作系统怎么把返回值给用户?- 寄存器或者用户传入的缓冲区地址
  • 系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法
  • 系统调用号的本质:数组下标!

可是为什么我们用的系统调用,从来没有见过什么 int 0x80 或者 syscall 呢?都是直接调用上层的函数的啊?
那是因为Linux的gnu C标准库,给我们把几乎所有的系统调用全部封装了。

  • #define SYS_ify(syscall_name) __NR_##syscall_name :是一个宏定义,用于将系统调用的名称转换为对应的系统调用号。比如: SYS_ify(open) 会被展开为 __NR_open
  • 而系统调用号,不是 glibc 提供的,是内核提供的,内核提供系统调用入口函数 man 2 syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头文件或者开发入口,让上层语言的设计者使用系统调用号,完成系统调用过程

4-3-5 缺页中断?内存碎片处理?除零野指针错误?

  • 缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

所以:

  • 操作系统就是躺在中断处理例程上的代码块!
  • CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱
  • CPU内部的软中断,比如除零/野指针等,我们叫做 异常。(所以,能理解"缺页异常"为什么这么叫了吗?)

4-4 如何理解内核态和用户态

结论:

  • 操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说操作系统系统调用方法的执行,是在进程的地址空间中执行的!
  • 关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念,而现在芯片为了保证兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性,这块我们不做深究了。
  • 用户态就是执行用户[0,3]GB时所处的状态
  • 内核态就是执行内核[3,4]GB时所处的状态
  • 区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
  • 一般执行 int 0x80 或者 syscall 软中断,CPL会在校验之后自动变更
  • 这样会不会不安全??

第五章:可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

第六章:volatile

该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下

[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig) {printf("chage flag 0 to 1\n");flag = 1;
}
int main() {signal(2, handler);while (!flag);printf("process quit normal\n");return 0;
}[hb@localhost code_test]$ cat Makefile
sig:sig.cgcc -o sig sig.c #-O2
.PHONY:clean
clean:rm - f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal

标准情况下,键入 CTRL-C,2号信号被捕捉,执行自定义动作,修改 flag=1,while 条件不满足,退出循环,进程退出

[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig) {printf("chage flag 0 to 1\n");flag = 1;
}
int main() {signal(2, handler);while (!flag);printf("process quit normal\n");return 0;
}[hb@localhost code_test]$ cat Makefile
sig:sig.cgcc -o sig sig.c -O2
.PHONY:clean
clean:rm - f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1

优化情况下,键入 CTRL-C,2号信号被捕捉,执行自定义动作,修改 flag=1,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显,while 循环检查的 flag,并不是内存中最新的 flag,这就存在了数据二异性的问题。while 检测的 flag 其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile

[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig) {printf("chage flag 0 to 1\n");flag = 1;
}
int main() {signal(2, handler);while (!flag);printf("process quit normal\n");return 0;
}[hb@localhost code_test]$ cat Makefile
sig:sig.cgcc -o sig sig.c -O2
.PHONY:clean
clean:rm - f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

第七章:SIGCHLD信号

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。请编写程序验证这样做不会产生僵尸进程。

验证子进程在退出的时候,会主动的向父进程发送SIGCHLD(17)信号

void handler(int signo) {cout << "I am process:" << getpid() << " , catch a signo:" << signo << endl;
}
int main() {signal(17, handler);pid_t id = fork();if (id == 0) {while (true) {cout << "I am child process: " << getpid() << " , ppid:" << getppid() << endl;sleep(1);break;}cout << "child quit." << endl;exit(0);}while (true) {cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}[sxy@VM-12-13-centos sig]$ ./mysignal 
I am father process:27240
I am child process: 27241 , ppid:27240
I am father process:27240
child quit.
I am process:27240 , catch a signo:17
I am father process:27240

将等待子进程写入到信号捕捉函数中(只有1个子进程)

void handler(int signo) {sleep(1);pid_t rid = waitpid(-1, nullptr, 0);cout << "I am process:" << getpid() << " , catch a signo:" << signo<< " child process quit:" << rid << endl;
}
int main() {signal(17, handler);pid_t id = fork();if (id == 0) {while (true) {cout << "I am child process: " << getpid() << " , ppid:" << getppid() << endl;sleep(3);break;}cout << "child quit." << endl;exit(0);}while (true) {cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}[sxy@VM-12-13-centos sig]$ ./mysignal 
I am father process:26883
I am child process: 26884 , ppid:26883
I am father process:26883
I am father process:26883
child quit.
I am father process:26883
^C

如果有10个子进程?如果同时退出?

//之前说过接收到信号的时候会自动屏蔽该信号,处理完才解除屏蔽
void handler(int signo) {sleep(1);pid_t rid;// while ((rid = waitpid(-1, nullptr, 0)) > 0) { //设置0,如果有进程没退,就会阻塞while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0) { //设置为非阻塞,没有进程退,返回值是0,退出循环cout << "I am process:" << getpid() << " , catch a signo:" << signo<< " child process quit:" << rid << endl;}
}
int main() {signal(17, handler);for (int i = 0; i < 10; i++) {pid_t id = fork();if (id == 0) {while (true) {cout << "I am child process: " << getpid() << " , ppid:" << getppid() << endl;sleep(3);break;}cout << "child quit." << endl;exit(0);}}while (true) {cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}[sxy@VM-12-13-centos sig]$ ./mysignal 
I am child process: 27596 , ppid:27595
I am child process: 27597 , ppid:27595
I am child process: 27598 , ppid:27595
I am child process: 27599 , ppid:27595
I am child process: 27600 , ppid:27595
I am child process: 27601 , ppid:27595
I am child process: 27602 , ppid:27595
I am father process:27595
I am child process: 27603 , ppid:27595
I am child process: 27605 , ppid:27595
I am child process: 27604 , ppid:27595
I am father process:27595
I am father process:27595
child quit.
child quit.
child quit.
child quit.
child quit.
child quit.
child quit.
child quit.
child quit.
child quit.
I am process:27595 , catch a signo:17 child process quit:27596
I am process:27595 , catch a signo:17 child process quit:27597
I am process:27595 , catch a signo:17 child process quit:27598
I am process:27595 , catch a signo:17 child process quit:27599
I am process:27595 , catch a signo:17 child process quit:27600
I am process:27595 , catch a signo:17 child process quit:27601
I am process:27595 , catch a signo:17 child process quit:27602
I am process:27595 , catch a signo:17 child process quit:27603
I am process:27595 , catch a signo:17 child process quit:27604
I am process:27595 , catch a signo:17 child process quit:27605

如果有10个子进程?如果不同时退出

//之前说过接收到信号的时候会自动屏蔽该信号,处理完才解除屏蔽
void handler(int signo) {srand(time(nullptr));sleep(1);pid_t rid;// while ((rid = waitpid(-1, nullptr, 0)) > 0) { //设置0,如果有进程没退,就会阻塞while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0) { //设置为非阻塞,没有进程退,返回值是0,退出循环cout << "I am process:" << getpid() << " , catch a signo:" << signo<< " child process quit:" << rid << endl;}
}
int main() {signal(17, handler);for (int i = 0; i < 10; i++) {pid_t id = fork();if (id == 0) {while (true) {cout << "I am child process: " << getpid() << " , ppid:" << getppid() << endl;sleep(1);break;}cout << "child quit." << endl;exit(0);}sleep(rand() % 3);}while (true) {cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}[sxy@VM-12-13-centos sig]$ ./mysignal 
I am child process: 28029 , ppid:28028
child quit.
I am child process: 28031 , ppid:28028
child quit.
I am process:28028 , catch a signo:17 child process quit:28029
I am child process: 28036 , ppid:28028
I am process:28028 , catch a signo:17 child process quit:28031
child quit.
I am process:28028 , catch a signo:17 child process quit:28036
I am child process: 28082 , ppid:28028
child quit.
I am child process: 28091 , ppid:28028
child quit.
I am process:28028 , catch a signo:17 child process quit:28082
I am child process: 28112 , ppid:28028
I am process:28028 , catch a signo:17 child process quit:28091
child quit.
I am process:28028 , catch a signo:17 child process quit:28112
I am child process: 28114 , ppid:28028
child quit.
I am process:28028 , catch a signo:17 child process quit:28114
I am child process: 28123 , ppid:28028
child quit.
I am process:28028 , catch a signo:17 child process quit:28123
I am father process:28028
I am child process: 28141 , ppid:28028
I am child process: 28142 , ppid:28028
I am father process:28028
child quit.
child quit.
I am process:28028 , catch a signo:17 child process quit:28141
I am process:28028 , catch a signo:17 child process quit:28142

必须要等待吗?必须要调用wait吗?

int main() {signal(17, SIG_IGN);for (int i = 0; i < 10; i++) {pid_t id = fork();if (id == 0) {while (true) {cout << "I am child process: " << getpid() << " , ppid:" << getppid() << endl;sleep(1);break;}cout << "child quit." << endl;exit(0);}sleep(1);}while (true) {cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}[sxy@VM-12-13-centos sig]$ ./mysignal 
I am child process: 28538 , ppid:28537
I am child process: 28540 , ppid:28537
child quit.
child quit.
I am child process: 28547 , ppid:28537
child quit.
I am child process: 28550 , ppid:28537
child quit.
I am child process: 28552 , ppid:28537
child quit.
I am child process: 28557 , ppid:28537
child quit.
I am child process: 28565 , ppid:28537
I am child process: 28569 , ppid:28537
child quit.
child quit.
I am child process: 28578 , ppid:28537
child quit.
I am child process: 28599 , ppid:28537
I am father process:28537
child quit.

当一个子进程正常退出(或异常终止)时,内核会自动向它的父进程发送一个 SIGCHLD 信号(17 号信号)。
如果父进程不处理这个信号,也没有 wait/waitpid,子进程的退出信息就会一直保存在内核中,变成僵尸进程。
内核对 SIGCHLD 有一个特殊规则:
如果 SIGCHLD 的处理动作被设置为 SIG_IGN(忽略),
则 子进程退出时不会进入僵尸状态,
内核会自动回收子进程的资源(PCB、退出码等)。
也就是说,你根本不用 wait 或 waitpid 去清理僵尸进程了,内核帮你干了这件事。

第八章:附录 - 课外阅读

用户态和内核态

  • CPU 指令集:是 CPU 实现软件指挥硬件执行的媒介,具体来说每一条汇编语句都对应了一条CPU 指令,而非常非常多的 CPU 指令在一起,可以组成一个、甚至多个集合,指令的集合叫 CPU 指令集。
  • CPU 指令集有权限分级,大家试想,CPU 指令集可以直接操作硬件的,要是因为指令操作的不规范,造成的错误会影响整个计算机系统的。好比你写程序,因为对硬件操作不熟悉,导致操作系统内核、及其他所有正在运行的程序,都可能会因为操作失误而受到不可挽回的错误,最后只能重启计算机才行。
    • 对开发人员来说是个艰巨的任务,还会增加负担,同时开发人员在这方面也不被信任,所以操作系统内核直接屏蔽开发人员对硬件操作的可能,都不让你碰到这些 CPU 指令集。

针对上面的需求,硬件设备商直接提供硬件级别的支持,做法就是对 CPU 指令集设置了权限,不同级别权限能使用的 CPU 指令集是有限的,以 Inter CPU 为例,Inter把 CPU 指令集操作的权限由高到低划为4级:

  • ring 0:权限最高,可以使用所有 CPU 指令集
  • ring 1
  • ring 2
  • ring 3:权限最低,仅能使用常规 CPU 指令集,不能使用操作硬件资源的 CPU 指令集,比如 IO 读写、网卡访问、申请内存都不行

要知道的是,Linux系统仅采用ring 0 和 ring 3 这2个权限。CPU中有一个标志字段,标志着线程的运行状态,用户态为3,内核态为0。

  • ring 0被叫做内核态,完全在操作系统内核中运行
    • 执行内核空间的代码,具有ring 0保护级别,有对硬件的所有操作权限,可以执行所有 CPU指令集,访问任意地址的内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机
  • ring 3被叫做用户态,在应用程序中运行
    • 在用户模式下,具有ring 3保护级别,代码没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存,在这种保护模式下,即使程序发生崩溃也是可以恢复的,在电脑上大部分程序都是在用户模式下运行的

低权限的资源范围较小,高权限的资源范围更大,所以用户态与内核态的概念就是CPU 指令集权限的区别。

我们通过指令集权限区分用户态和内核态,还限制了内存资源的使用,操作系统为用户态与内核态划分了两块内存空间,给它们对应的指令集使用。

在内存资源上的使用,操作系统对用户态与内核态也做了限制,每个进程创建都会分配虚拟空间地址,以Linux32位操作系统为例,它的寻址空间范围是 4G (2的32次方),而操作系统会把虚拟控制地址划分为两部分,一部分为内核空间,另一部分为用户空间,高位的 1G (从虚拟地址0xC0000000 到 0xFFFFFFFF)由内核使用,而低位的 3G (从虚拟地址 0x00000000 到0xBFFFFFFF)由各个进程使用。

  • 用户态:只能操作 0-3G 范围的低位虚拟空间地址
  • 内核态: 0-4G 范围的虚拟空间地址都可以操作,尤其是对 3-4G 范围的高位虚拟空间地址必须由内核态去操作
    • 3G-4G 部分大家是共享的(指所有进程的内核态逻辑地址是共享同一块内存地址),是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。
    • 在内核运行的过程中,会涉及内核栈的分配,内核的进程管理的代码会将内核栈创建在内核空间中,当然相应的页表也会被创建。

用户态与内核态的切换

什么情况会导致用户态到内核态切换??

  • 系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用。
    • 操作系统提供了中断指令int 0x80来主动进入内核,这是用户程序发起的调用访问内核代码的唯一方式。调用系统函数时会通过内联汇编代码插入int 0x80的中断指令,内核接收到int0x80中断后,查询中断处理函数地址,随后进入系统调用。
  • 异常:当 CPU 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常
  • 中断:当 CPU 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。

切换时 CPU 需要做什么??

  • 当某个进程中要读写 IO ,必然会用到 ring 0 级别的 CPU 指令集 。而此时 CPU 的指令集操作权限只有 ring 3,为了可以操作ring 0 级别的 CPU 指令集 , CPU 切换指令集操作权限级别为ring 0(可称之为提权),CPU再执行相应的ring 0 级别的 CPU 指令集 (内核代码)。
  • 代码发生提权时,CPU 是需要切换栈的!!前面我们提到过,内核有自己的内核栈。CPU 切换栈是需要栈段描述符(ss寄存器)和栈顶指针(esp寄存器),这两个值从哪里来?
    • CPU通过一个段寄存器(tr)确定 TSS(任务状态段,struct TSS) 的位置。在TSS结构中存在这么一个 SS0 和 ESP0。提权的时候,CPU就从这个TSS里把SS0和ESP0取出来,放到 ss和 esp 寄存器中。

切换流程

  1. 从用户态切换到内核态时,首先用户态可以直接读写寄存器,用户态操作CPU,将寄存器的状态保存到对应的内存中,然后调用对应的系统函数,传入对应的用户栈地址和寄存器信息,方便后续内核方法调用完毕后,恢复用户方法执行的现场。
  2. 从用户态切换到内核态需要提权,CPU 切换指令集操作权限级别为 ring 0。
  3. 提权后,切换内核栈。然后开始执行内核方法,相应的方法栈帧是保存在内核栈中。
  4. 当内核方法执行完毕后,CPU切换指令集操作权限级别为 ring 3,然后利用之前写入的信息来恢复用户栈的执行。

从上述流程可以看出用户态切换到内核态的时候,会牵扯到用户态现场信息的保存以及恢复,还要进行一系列的安全检查,还是比较耗费资源的。

作业

1. 以下描述正确的有:[多选]

A.使用kill -l命令可以查看Linux系统中信号的种类
B.使用kill -p命令可以查看Linux系统种的信号种类
C.使用Ctrl+C会使当前终端前台进程退出
D.使用Ctrl+Z会使当前终端前台进程退出

答案:AC
A正确,B错误, 因为kill - l 选项才是查看信号种类的选项方式
C正确,Ctrl + C会向当前终端的前台进程发送SIGINT信号,中断正在运行在前台的程序
D错误 Ctrl + Z会使当前终端的前台进程进入停止态而不是退出

2. 以下描述正确的有: [多选]

A.程序运行过程中产生异常会产生信号
B.通过终端键盘Ctlr+C可以产生一个SIGQUIT信号
C.通过系统调用接口raise只能产生一个SIGABRT信号
D.通过alarm接口可以在n秒钟之后产生一个SIGALRM信号

答案:AD
A正确,程序运行异常退出,也是系统检测到异常后向进程发送指定的异常信号,程序对信号做出的处理方式就是退出所导致的
B错误 Ctrl + C产生SIGINT信号,而并非SIGQUIT信号
C错误 raise接口可以向当前调用进程发送任意信号
D正确 alarm函数相当于设置一个定时器,在n秒钟后向进程发送SIGALRM信号

3. 以下描述正确的有:[多选]

A.若信号被阻塞,则信号将无法添加到未决信号集合中
B.若信号被阻塞,则信号依然可以添加到未决信号集合中
C.若信号被忽略,则信号将无法添加到未决信号集合中
D.若信号被忽略,则信号依然可以添加到未决信号集合中

答案:BD
信号阻塞就是可以继续接受信号,但是暂时不处理指定信号,实现原理就是在block阻塞信号集合中标记指定的信号,被标记的信号收到后暂时不处理
因此A错误,B正确
信号忽略,只是说信号的处理方式就是忽略处理,并不表示不再接收指定信号
因此C错误,D正确

4. 以下描述正确的有:

A.未决信号指的是已经被处理的信号
B.未决信号指的是还未被处理的信号
C.同一个信号可以在未决信号集合中添加多次
D.每一个信号处理完毕后都会从pending集合中移除

答案:B
未决信号指的是,进程收到了信号,被添加到未决信号集合中,但是暂时还没有被处理的信号。
因此A错误,B正确
非可靠信号在进行注册时,会查看是否已经有相同信号添加到未决集合中,如果有则什么都不做,因此非可靠信号只会添加一次,因此处理完毕后会直接移除(准确来说是先移除,后处理)。而可靠信号会重复添加信号信息到sigqueue链表中,相当于可靠信号可以重复添加,处理完毕后,因为有可能还有相同的信号信息待处理,因此并不会直接移除,而是检测没有相同信号信息后才会从pending集合中移除
因此C和D选项错误

5. 以下哪些信号无法被阻塞 [多选]

A.SIGKILL
B.SIGINT
C.SIGSTOP
D.SIGQUIT

答案:AC
SIGSTOP/SIGKILL信号无法被阻塞,无法被自定义,无法被忽

6. 一个进程无法被kill杀死的可能有哪些?[多选]

A.这个进程阻塞了信号
B.用户有可能自定义了信号的处理方式
C.这个进程有可能是僵尸进程
D.这个进程当前状态是停止状态

答案:ABCD
A正确 信号被阻塞,则暂时不被处理(SIGKILL / SIGSTOP除外,因为无法被阻塞,这里说的是可能性,因此不做太多纠结)
B正确 自定义处理之后,信号的处理方式有可能不再是进程退出
C正确 僵尸进程因为已经退出,因此不做任何处理
D正确 进程停止运行,则将不再处理信号

7.  以下描述正确的有:

A.只能使用signal函数自定义信号捕捉函数
B.若当前进程处于阻塞状态,则此时到来的信号暂时无法处理。
C.使用signal函数进行信号捕捉函数修改时,可以指定SIG_DFL设置为忽略处理
D.使用signal函数进行信号捕捉函数修改时,可以指定SIG_IGN设置为忽略处理

答案:D
A sigaction接口也可以自定义信号处理方式
B 信号会打断进程当前的阻塞状态去处理信号
C SIG_DFL为信号默认处理方式

8. 下列选项中,会导致用户进程从用户态切换到内核态的操作是()。

I.整数除以零
II.sin()函数调用
III.read 系统调用

A.仅 I、 II
B.仅 I、 III
C.仅 II、 III
D.I、 II 和 III

答案:B
程序运行从用户态切换到内核态的操作:中断 / 异常 / 系统调用
I 会导致程序异常(分母不能为0)
II 库函数并不会引起运行态的切换
III 系统调用接口
因此只有 I 和 III 符合条件。

9. 以下描述正确的有:

A.所有的未决信号都会立即被处理
B.自定义处理方式的信号会返回用户态执行信号捕捉函数
C.所有的信号处理方式都是在用户态完成信号捕捉的
D.自定义处理方式的信号会在执行完信号捕捉函数后直接返回用户态主控流程

答案:B
A错误 未决信号是在从程序运行在内核态返回用户态的时候进行处理
B正确 自定义处理方式会执行用户定义的处理函数,因此会返回用户态运行
C错误 只有自定义处理方式的信号会在用户态进行处理
D错误 自定义处理方式的信号会在执行完信号捕捉函数后先返回内核态

10. 以下描述正确的有:

A.在一个函数中若对局部变量进行了操作,则这个函数一定是不可重入的
B.在一个函数中若对全局变量进行了操作,则这个函数一定是可重入的
C.在一个函数中若对全局变量进行了原子操作,则这个函数一定是可重入的
D.在一个函数中若对局部变量进行了原子操作,则这个函数一定是不可重入的

答案:C
函数是否可重入的关键在于函数内部是否对全局数据进行了不受保护的非原子操作,其中原子操作指的是一次完成,中间不会被打断的操作,表示操作过程是安全的
因此如果一个函数中如果对全局数据进行了原子操作,但是因为原子操作本身是不可被打断的,因此他是可重入的
A错误,函数的重入对局部变量并无影响
B错误,全局变量的操作若并非原子操作,则有可能会出问题
C正确
D错误,局部变量的操作本身就不影响重入,所以函数是可重入的

11. 以下描述正确的有 [多选]

A.函数可重入指的是函数中可以在不同的执行流中调用函数会出现数据二义问题
B.函数不可重入指的是函数中可以在不同的执行流中调用函数会出现数据二义问题
C.函数不可重入指的是函数中可以在不同的执行流中调用函数而不会出现数据二义问题
D.函数可重入指的是函数中可以在不同的执行流中调用函数而不会出现数据二义问题

答案:BD
函数的重入指的是一个函数在不同执行流中同时进入运行,其中不可重入指的是一旦重入就有可能会出问题,而可重入就是不管怎么重入都不会有特殊影响
根据对函数可重入与不可重入的理解分析,正确选项为 B和D 选项

12. 两个线程并发执行以下代码,假设a是全局变量,初始为1,那么以下输出______是可能的?[多选] 

void foo() {a = a + 1;printf("%d ", a);
}

A.3 2
B.2 3
C.3 3
D.2 2

答案:ABCD
当函数内的操作非原子时因为竞态条件造成的数据二义
a = a + 1 和 printf("%d", a) 之间有可能会被打断
a初始值为1
当A线程执行完a = a + 1后a是2,这时候打印会打印2, 线程B执行时 + 1打印3
当A线程执行完a = a + 1后a是2,这时候时间片轮转到B线程,进行 + 1打印3, 然后时间片轮转回来也打印3
这两个是比较显而易见的,但是还有一些 特殊情况需要注意

  1. printf函数实际上并不是直接输出,而是把数据放入缓冲区中,因此有可能A线程将打印的2放入缓冲区中,还没来得及输出,这时候B线程打印了3,时间片轮转回来就会后打印2
  2. a = a + 1本身就不是原子操作因此有可能同时进行操作,都向寄存器中加载1进去,然后进行 + 1后,将2放回内存,因此有可能会打印2和2
http://www.dtcms.com/a/474966.html

相关文章:

  • 中小企业网站制作是什么网站php源码
  • MySQL笔记12
  • 改变网站的域名空间国家示范校建设网站
  • HTML 01 【基础语法学习】
  • 从UI设计师到“电影魔术师”:After Effects中的FUI(科幻电影界面)设计工作流
  • 如何修改PPT输出图片的分辨率,提高清晰度
  • 做便民网站都需要提供什么电商网站前端模板
  • 菏泽市城乡和建设局网站wordpress主题简
  • 多屏合一网站建设网络运营者不得泄露
  • 控制台字符动画-小球弹跳
  • 轻松Linux-11.线程(上)
  • 使用 MQ 解决分布式事务一致性问题
  • 中国石化工程建设公司网站保山市建设厅官方网站
  • 电子商务网站建设需要哪些步骤聚成网站建设
  • 前端开发指南,前端开发需要学什么
  • 一个网站设计的费用搭建影视网站违法
  • 深入解析 YOLO v2
  • 网站建设开票计量单位网站建设宣传素材
  • wordpress插件怎么使用兰州搜索引擎优化
  • Mysql初阶第七讲:Mysql复合查询
  • 代码随想录 101.对称二叉树
  • 深圳网站建设哪家比较专业大学城网站开发公司
  • 制作静态链接库并使用
  • 西方设计网站vs2010网站开发与发布
  • 网站维护工作的基本内容施工企业质量管理体系认证几年
  • 网站建设教程pdf下载企业官网网页设计
  • 重启MySQL,为何重启后MySQL数据“回滚”了?
  • 非洲购物网站排名文创产品设计包括哪些方面
  • Linux 文件内容查看与编辑
  • 2022 CSP-J复赛题