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

Linux系统--进程信号

大家好,今天我们来继续学习Linux的部分,上次我们学习了有关进程间通信的内容,今天我们来学习一下信号部分,那么话不多说,我们开始今天的内容。

目录

Linux进程信号

1. 信号快速认识

1-1 ⽣活⻆度的信号

1-2 技术应⽤⻆度的信号

1-2-1 ⼀个样例

1-2-2 ⼀个系统函数

1-3 信号概念

1-3-1 查看信号

1-3-2 信号处理

2. 产⽣信号

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

2-1-1 基本操作

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

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

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

2-3 使⽤函数产⽣信号

2-3-1 kill

2-3-2 raise

2-3-3 abort

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

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

2-4-2 设置重复闹钟

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

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

2-5 硬件异常产⽣信号

2-5-1 模拟除0

2-5-2 模拟野指针

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

2-5-4 Core Dump

2-6 总结思考⼀下

3. 保存信号

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

3-2 在内核中的表⽰

3-3 sigset_t

3-4 信号集操作函数

3-4-1 sigprocmask

3-4-2 sigpending

4. 捕捉信号

4-1 信号捕捉的流程

4-2 sigaction

4-3 操作系统是怎么运⾏的

4-3-1 硬件中断

4-3-2 时钟中断

4-3-3 死循环

4-3-4 软中断

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

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

5. 可重⼊函数

6. volatile

7. SIGCHLD信号


Linux进程信号

1. 信号快速认识

1-1 ⽣活⻆度的信号

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

看了这个例子,我们可以进行一下总结:

  1. 信号处理,进程在信号没有产生的时候,早就知道信号该如何处理了 → 人,必须把要处理事情记录下来!
  2. 信号的处理,不是立即处理,而是可以等一会在处理,合适的时候,进行信号的处理
  3. 人能识别信号,是提前被 “教育” 过的,进程也是如此,OS 程序员设计的进程,进程早已经内置了对于信号的识别和处理方式!
  4. 信号源非常多→给进程产生信号的,信号源,也非常多!
当我们收到信号但是不想立即执行的时候,那就要将信号保存起来,所以对信号可以分为三部分:
信号到来 | 信号保存 | 信号处理
怎么进⾏信号处理啊?
a.默认 b.忽略 c.⾃定义, 后续都叫做信号捕捉。
1-2 技术应⽤⻆度的信号
1-2-1 ⼀个样例
#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!

我们自定义handler函数来修改2号信号的处理方法,程序运行开始后,当我们摁下Ctrl+C时,该信号就会被捕获,然后执行我们的handler方法

我们刚才的例子中使用Ctrl+C后进程不是退出了没,为什么这里进程没有退出呢?

2号信号的默认处理方法时结束进程,而在这里我们自定义了2号信号的处理方法,所以不会再退出了

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

关于前台进程和后台进程的区别,前台进程和后台进程都可以向标准输出中打印,但只有前台进程能从标准输入中获取内容,

1-3 信号概念

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

1-3-1 查看信号

我们使用kill -l命令可以查看所有信号:

每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义
#define SIGINT 2
编号34以上的是实时信号,本文只讨论编号34以下的信号,不讨论实时信号。这些信号各⾃在什么条件下产⽣,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

1-3-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*/, SIG_IGN); // 设置忽略信号的宏while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
执⾏该信号的默认处理动作
#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);}
}
提供⼀个信号处理函数,要求内核在处理该信号时切换到⽤⼾态执⾏这个处理函数,这种⽅式称为⾃
定义捕捉(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);}
}
上⾯的所有内容,我们都没有做⾮常多的解释,主要是先⽤起来,然后渗透部分概念和共识,下⾯我们从理论和实操两个层⾯,来进⾏对信号的详细学习、论证和理解。为了保证条理,我们采⽤如下思路来进⾏阐述:

2. 产⽣信号

当前阶段:
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);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);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

jobs是查看当前所有后台进程

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

当键盘有输入时,硬件会向CPU发送硬件中断,CPU接受到该信号后就会处理键盘的输入

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

信号其实是从纯软件⻆度,模拟硬件中断的⾏为
只不过硬件中断是发给CPU,⽽信号是发给进程
两者有相似性,但是层级不同,这点我们后⾯的感觉会更加明显
2-2 调⽤系统命令向进程发信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{while(true){sleep(1);}
}
⾸先在后台执⾏死循环程序,然后⽤kill命令给它发SIGSEGV信号
$ 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 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 命令
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
// mykill -signumber pid
int main(int argc, char *argv[])
{if(argc != 3){std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;return 1;}int number = std::stoi(argv[1]+1); // 去掉-pid_t pid = std::stoi(argv[2]);int n = kill(pid, number);return n;
}
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.
样例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{// 整个代码就只有这⼀处打印std::cout << "获取了⼀个信号: " << signumber << std::endl;
}
// mykill -signumber pid
int main()
{signal(2, handler); // 先对2号信号进⾏捕捉// 每隔1S,⾃⼰给⾃⼰发送2号信号while(true){sleep(1);raise(2);}
}$ g++ raise.cc -o raise
$ ./raise
获取了⼀个信号: 2
获取了⼀个信号: 2
获取了⼀个信号: 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函数总是会成功的,所以没有返回值。
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int signumber)
{// 整个代码就只有这⼀处打印std::cout << "获取了⼀个信号: " << signumber << std::endl;
}
// mykill -signumber pid
int main()
{signal(SIGABRT, handler);while(true){sleep(1);abort();}
}$ g++ Abort.cc -o Abort
$ ./Abort
获取了⼀个信号: 6 // 实验可以得知,abort给⾃⼰发送的是固定6号信号,虽然捕捉了,但是还是要退出
Aborted// 注释掉15⾏代码
$ ./Abort
Aborted
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 previ‐ously 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
user:~/code/test$ ./alarm
count : 492333713
闹钟会响⼀次,默认终⽌进程
有IO效率低
2-4-2 设置重复闹钟
代码样例
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>
using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;
// 把信号 更换 成为 硬件中断
void hanlder(int signo)
{for(auto &f : gfuncs){f();}std::cout << "gcount : " << gcount << std::endl;int n = alarm(1); // 重设闹钟,会返回上⼀次闹钟的剩余时间std::cout << "剩余时间 : " << n << std::endl;
}int main()
{//gfuncs.push_back([](){ std::cout << "我是⼀个内核刷新操作" << std::endl; });//gfuncs.push_back([](){ std::cout << "我是⼀个检测进程时间⽚的操作,如果时间⽚到了,我会切换进程" << std::endl; });//gfuncs.push_back([](){ std::cout << "我是⼀个内存管理操作,定期清理操作系统内部的内存碎⽚" << std::endl; });alarm(1); // ⼀次性的闹钟,超时alarm会⾃动被取消signal(SIGALRM, hanlder);while (true){pause();std::cout << "我醒来了..." << std::endl;gcount++;}
}
NAMEpause - wait for signal
SYNOPSIS#include <unistd.h>int pause(void);
DESCRIPTIONpause() causes the calling process (or thread) to sleep until a signalis delivered that either terminates the process or causes the invoca‐tion of a signal-catching function.
RETURN VALUEpause() returns only when a signal was caught and the signal-catchingfunction returned. In this case, pause() returns -1, and errno is set to EINTR.

我们在另一个进程中让alarm信号提前触发

// 窗⼝ 2

$ kill -14 216982
// 窗⼝ 1
user:~/code/test$ ./alarm
我的进程pid: 216982
剩余时间 : 13 // 提前唤醒它,剩余
时间
剩余时间 : 0
剩余时间 : 0
剩余时间 : 0

后面的重复闹钟设置时返回值是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
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{printf("catch a sig : %d\n", sig);
}
// v1
int main()
{//signal(SIGFPE, handler); // 8) SIGFPEsleep(1);int a = 10;a/=0;while(1);return 0;
}
2-5-2 模拟野指针
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{printf("catch a sig : %d\n", sig);
}
int main()
{//signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while(1);return 0;
}
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。
通过上⾯的实验,我们可能发现:
发现⼀直有8号信号产⽣被我们捕获,这是为什么呢?上⾯我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应⽤程序的异常情况,其实在CPU中有⼀些控制和状态寄存器,主要⽤于控制处理器的操作,通常由操作系统代码使⽤。状态寄存器可以简单理解为⼀个位图,对应着⼀些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调⽤对应的异常处理⽅法。
除零异常后,我们并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以CPU中还
保留上下⽂数据以及寄存器内容,除零异常会⼀直存在,就有了我们看到的⼀直发出异常信
号的现象。访问⾮法内存其实也是如此,⼤家可以⾃⾏实验。

说了这么多,那么操作系统是怎么知道犯错了呢?

当进程出现错误时,首先操作系统要识别进程是否出错,然后判断出了哪种类型的错误,再向进程发送对应的信号,CPU的寄存器中有一个状态寄存器,这里记录着进程是否出现溢出错误等信息

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

#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
int main()
{if (fork() == 0){sleep(1);int a = 10;a /= 0;exit(0);}int status = 0;waitpid(-1, &status, 0);printf("exit signal: %d, core dump: %d\n", status&0x7F, (status>>7)&1);return 0;
}
2-5-4 Core Dump
1. SIGINT的默认处理动作是终⽌进程,SIGQUIT的默认处理动作是终⽌进程并且Core Dump,现在我们来验证⼀下。
2. ⾸先解释什么是Core Dump。当⼀个进程要异常终⽌时,可以选择把进程的⽤⼾空间内存数据全部保存到磁盘上,⽂件名通常是core,这叫做Core Dump。
3. 进程异常终⽌通常是因为有Bug,⽐如⾮法内存访问导致段错误,事后可以⽤调试器检查core⽂件以查清错误原因,这叫做 Post-mortem Debug (事后调试)。

4. ⼀个进程允许 产⽣多⼤的 core ⽂件取决于进程的 Resource Limit (这个信息保存 在PCB

中)。默认是不允许产⽣ core ⽂件的, 因为 core ⽂件中可能包含⽤⼾密码等敏感信息,不安全。
5. 在开发调试阶段可以⽤ ulimit 命令改变这个限制,允许产⽣ core ⽂件。 ⾸先⽤ ulimit 命令
改变 Shell 进程的 Resource Limit ,如允许 core ⽂件最⼤为 1024K: $ ulimit -c 1024

不同的信号在执行后有不同的作用,Term表示进程接收到相关信号后,系统将终止该进程的运行。它是一种常用的正常终止进程的信号,接收Term相关信号并终止进程时,不会生成核心转储文件。

当进程接收到如SIGABRT(由abort(3)函数引发的终止信号 )、SIGSEGV(段错误,非法内存访问 )等会产生Core标识的信号时,系统会在当前路径下形成一个文件,把进程在内存中的核心数据从内存拷贝到磁盘,形成核心转储文件 。

简单来说,Core重点在于异常终止进程并保留进程终止时的内存数据用于后续调试,而Term侧重于终止进程,通常用于正常的进程关闭场景。

1 $ ulimit -c 1024
2 $ ulimit -a
3 core file size             (blocks, -c) 1024
4 data seg size              (kbytes, -d) unlimited
5 scheduling priority                (-e) 0
6 file size                  (blocks, -f) unlimited
7 pending signals                    (-i) 7643
8 max locked memory          (kbytes, -l) 65536
9 max memory size            (kbytes, -m) unlimited
10 open files                        (-n) 65535
11 pipe size              (512 bytes, -p) 8
12 POSIX message queues       (bytes, -q) 819200
13 real-time priority                (-r) 0
14 stack size                (kbytes, -s) 8192
15 cpu time                 (seconds, -t) unlimited
16 max user processes                (-u) 7643
17 virtual memory            (kbytes, -v) unlimited
18 file locks                        (-x) unlimited
然后写⼀个死循环程序:

前台运⾏这个程序,然后在终端键⼊Ctrl-C(貌似不⾏)或Ctrl-\(介个可以):

ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制⽽来,所以也具 有和
Shell进程相同的Resource Limit值,这样就可以产⽣Core Dump了。 使⽤core⽂件:

2-6 总结思考⼀下
上⾯所说的所有信号产⽣,最终都要有OS来进⾏执⾏,为什么?OS是进程的管理者
信号的处理是否是⽴即处理的?在合适的时候
信号如果不是被⽴即处理,那么信号是否需要暂时被进程记录下来?记录在哪⾥最合适呢?
⼀个进程在没有收到信号的时候,能否能知道,⾃⼰应该对合法信号作何处理呢?
如何理解OS向进程发送信号?能否描述⼀下完整的发送处理过程?

3. 保存信号

当前阶段
3-1 信号其他相关常⻅概念
实际执⾏信号的处理动作称为信号递达(Delivery)
信号从产⽣到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。
3-2 在内核中的表⽰
信号在内核中的表示意图
1. 每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
2. SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
3. SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤⼾⾃定义函数
sighandler。如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?POSIX.1允许系统递送该信号⼀次或多次。Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。本章不讨论实时信号。

三张表用来表示信号,block表的比特位位置表示第几个信号,比特位内容表示信号是否被阻塞,pending表的比特位位置表示第几个信号,比特位内容表示是否接收到信号,handler表表示信号的处理方法,有默认,忽略,自定义三种处理方法

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
调⽤函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指针,则更改进程的信号屏蔽字,参数how指⽰如何更改。如果oset和set都是⾮空指针,则先将原来的信号屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀个信号递达。
3-4-2 sigpending
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调⽤成功则返回0,出错则返回-1
下⾯⽤刚学的⼏个函数做个实验。程序如下:
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
void PrintPending(sigset_t &pending)
{std::cout << "curr process[" << getpid() << "]pending: ";for (int signo = 31; signo >= 1; signo--){if (sigismember(&pending, signo)){std::cout << 1;}else{std::cout << 0;}}std::cout << "\n";
}void handler(int signo)
{std::cout << signo << " 号信号被递达!!!" << std::endl;std::cout << "-------------------------------" << std::endl;sigset_t pending;sigpending(&pending);PrintPending(pending);std::cout << "-------------------------------" << std::endl;
}int main()
{// 0. 捕捉2号信号signal(2, handler); // ⾃定义捕捉//signal(2, SIG_IGN); // 忽略⼀个信号//signal(2, SIG_DFL); // 信号的默认处理动作// 1. 屏蔽2号信号sigset_t block_set, old_set;sigemptyset(&block_set);sigemptyset(&old_set);sigaddset(&block_set, SIGINT); // 我们有没有修改当前进⾏的内核block表呢???1 0// 1.1 设置进⼊进程的Block表中sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进⾏的内核block表,完成了对2号信号的屏蔽!int cnt = 15;while (true){// 2. 获取当前进程的pending信号集sigset_t pending;sigpending(&pending);// 3. 打印pending信号集PrintPending(pending);cnt--;// 4. 解除对2号信号的屏蔽if (cnt == 0){std::cout << "解除对2号信号的屏蔽!!!" << std::endl;sigprocmask(SIG_SETMASK, &old_set, &block_set);}sleep(1);}
}$ ./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
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010

由于我们已经给2号信号进行了屏蔽,所以我们使用Ctrl+c的时候,2号信号会被阻塞,不会被递达

4. 捕捉信号

当前阶段
4-1 信号捕捉的流程

信号的处理,不是立即处理,可以等到合适的时候再处理

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

上图中上半部分代表用户态,下半部分代表内核态,如果要在合适的时候进行信号的处理,那么应该是什么时候呢?

当代码在用户态运行时,当出现中断,异常或系统调用的时候会由用户态进入内核态,在内核处理完异常之后,会检测当前有没有可以递达的信号,此时就会进行处理,如果信号的处理方法是默认或忽略时,由内核完成,若是处理方法为自定义,则需要从内核态先返回到用户态进行自定义处理方法的运行,在信号处理之后,要在内核态统一返回用户态继续代码的执行。

1. ⽤户程序注册了 SIGQUIT 信号的处理函数 sighandler
2. 当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态。
3. 在中断处理完毕后要返回⽤户态的 main 函数之前检查到有信号 SIGQUIT 递达。
4. 内核决定返回⽤⼾态后不是恢复 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 函数, sighandler main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独⽴的控制流程。
5. sighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态。
6. 如果没有新的信号要递达,这次再返回⽤户态就是恢复 main 函数的上下⽂继续执⾏了。

其中检查pending表是在内核态进行的

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的结构体:
struct sigaction {void     (*sa_handler)(int);void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t   sa_mask;int        sa_flags;void     (*sa_restorer)(void);  /* 已过时,不要使用 */
};
将sa_handler赋值为常数SIG_IGN传给sigaction表⽰忽略信号,赋值为常数SIG_DFL表⽰执⾏系统默认动作,赋值为⼀个函数指针表⽰⽤⾃定义函数捕捉信号,或者说向内核注册了⼀个信号处理函数,该函数返回值为void,可以带⼀个int参数,通过参数可以得知当前信号的编号,这样就可以⽤同⼀个函数处理多种信号。显然,这也是⼀个回调函数,不是被main函数调⽤,⽽是被系统所调⽤。
sa_mask表示在信号处理期间,需要被屏蔽的信号,在处理当前信号时,这些被屏蔽的信号不会被立即处理,直到信号处理函数返回
4-3 操作系统是怎么运⾏的
4-3-1 硬件中断

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

此后,操作系统就不需要关注外部设备了,当外设准备好时,会向CPU发送信号

信号本质上就是用软件来模拟硬件中断,发送中断就是发送信号,记录中断就是记录信号,中断号就是信号编号,处理中断就是处理信号

4-3-2 时钟中断
问题:
进程可以在操作系统的指挥下,被调度,被执⾏,那么操作系统⾃⼰被谁指挥,被谁推动执⾏呢?
外部设备可以触发硬件中断,但是这个是需要⽤户或者设备⾃⼰触发,有没有⾃⼰可以定期触发的设备?

硬件需要手动触发才能被执行,那我们在CPU内部设置一个时钟源,让他定期触发,不久可以了吗?

所以操作系统就在硬件时钟的驱动下,进行调度了。也就是说,操作系统是一个基于硬件中断进行工作的软件,当没有中断的时候,操作系统是暂停的,什么都没有做。

4-3-3 死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可.操作系统的本质:就是⼀个死循环
void main(void) /* 这⾥确实是void,并没错。 */
{   /* 在startup 程序(head.s)中就是这样假设的。 */.../** 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没有的话我们就回到这⾥,⼀直循环执⾏'pause()'。*/for (;;)pause();
} // end main
这样,操作系统,就可以在硬件时钟的推动下,⾃动调度了

时钟源的时钟周期由CPU的主频决定,主频越快,时钟周期越短,CPU效率越高,时间片由多个时钟周期组成,当进程的时间片到期时,会进行进程调度

4-3-4 软中断

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

由图可知,当我们使用系统调用时,会触发软中断,那么现在就会有几个问题了:

1. ⽤户层怎么把系统调⽤号给操作系统? 
sys_call_table是系统调用函数指针表,表里的下标就是系统调用号,CPU内部有寄存器是专门用来存储系统调用号
2. 操作系统怎么把返回值给⽤户?
在许多计算机架构中,如 x86、ARM 等,都定义了特定的寄存器用于传递函数调用(包括系统调用)的返回值。 当系统调用执行完毕,内核会将返回值放置到这些约定好的寄存器中,然后将控制权交回给用户态的应用程序。应用程序从相应寄存器中读取返回值,完成对系统调用结果的获取。
系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法
系统调⽤号的本质:数组下标
// sys.h
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read (); // 读⽂件。 (fs/read_write.c, 55)
extern int sys_write (); // 写⽂件。 (fs/read_write.c, 83)
extern int sys_open (); // 打开⽂件。 (fs/open.c, 138)
extern int sys_close (); // 关闭⽂件。 (fs/open.c, 192)
extern int sys_waitpid (); // 等待进程终⽌。 (kernel/exit.c, 142)
extern int sys_creat (); // 创建⽂件。 (fs/open.c, 187)
extern int sys_link (); // 创建⼀个⽂件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink (); // 删除⼀个⽂件名(或删除⽂件)。 (fs/namei.c, 663)
extern int sys_execve (); // 执⾏程序。 (kernel/system_call.s, 200)
extern int sys_chdir (); // 更改当前⽬录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod (); // 建⽴块/字符特殊⽂件。 (fs/namei.c, 412)
extern int sys_chmod (); // 修改⽂件属性。 (fs/open.c, 105)
extern int sys_chown (); // 修改⽂件宿主和所属组。 (fs/open.c, 121)
extern int sys_break (); // (-kernel/sys.c, 21)
extern int sys_stat (); // 使⽤路径名取⽂件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek (); // 重新定位读/写⽂件偏移。 (fs/read_write.c, 25)
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount (); // 安装⽂件系统。 (fs/super.c, 200)
extern int sys_umount (); // 卸载⽂件系统。 (fs/super.c, 167)
extern int sys_setuid (); // 设置进程⽤⼾id。 (kernel/sys.c, 143)
extern int sys_getuid (); // 取进程⽤⼾id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间⽇期。 (-kernel/sys.c, 148)
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat (); // 使⽤⽂件句柄取⽂件的状态信息。(fs/stat.c, 47)
extern int sys_pause (); // 暂停进程运⾏。 (kernel/sched.c, 144)
extern int sys_utime (); // 改变⽂件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty (); // 修改终端⾏设置。 (-kernel/sys.c, 31)
extern int sys_gtty (); // 取终端⾏设置信息。 (-kernel/sys.c, 36)
extern int sys_access (); // 检查⽤⼾对⼀个⽂件的访问权限。(fs/open.c, 47)
extern int sys_nice (); // 设置进程执⾏优先权。 (kernel/sched.c, 378)
extern int sys_ftime (); // 取⽇期和时间。 (-kernel/sys.c,16)
extern int sys_sync (); // 同步⾼速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill (); // 终⽌⼀个进程。 (kernel/exit.c, 60)
extern int sys_rename (); // 更改⽂件名。 (-kernel/sys.c, 41)
extern int sys_mkdir (); // 创建⽬录。 (fs/namei.c, 463)
extern int sys_rmdir (); // 删除⽬录。 (fs/namei.c, 587)
extern int sys_dup (); // 复制⽂件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe (); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times (); // 取运⾏时间。 (kernel/sys.c, 156)
extern int sys_prof (); // 程序执⾏时间区域。 (-kernel/sys.c, 46)
extern int sys_brk (); // 修改数据段⻓度。 (kernel/sys.c, 168)
extern int sys_setgid (); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid (); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal (); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid (); // 取进程有效⽤⼾id。 (kenrl/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct (); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys (); // (-kernel/sys.c, 82)
extern int sys_lock (); // (-kernel/sys.c, 87)
extern int sys_ioctl (); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl (); // ⽂件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx (); // (-kernel/sys.c, 92)
extern int sys_setpgid (); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit (); // (-kernel/sys.c, 97)
extern int sys_uname (); // 显⽰系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认⽂件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat (); // 取⽂件系统信息。 (fs/open.c, 19)
extern int sys_dup2 (); // 复制⽂件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid (); // 取⽗进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp (); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)
extern int sys_setsid (); // 在新会话中运⾏程序。 (kernel/sys.c, 206)
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid (); // 设置真实与/或有效⽤⼾id。 (kernel/sys.c,118)
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51)// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};// 调度程序的初始化⼦程序。
void sched_init(void)
{...// 设置系统调⽤中断⻔。set_system_gate(0x80, &system_call);
}_system_call:cmp eax,nr_system_calls-1 ;// 调⽤号如果超出范围的话就在eax 中置-1 并退出。ja bad_sys_callpush ds ;// 保存原段寄存器值。push espush fspush edx ;// ebx,ecx,edx 中放着系统调⽤相应的C 语⾔函数的调⽤参数。push ecx ;// push %ebx,%ecx,%edx as parameterspush ebx ;// to the system callmov edx,10h ;// set up ds,es to kernel spacemov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。mov es,dxmov edx,17h ;// fs points to local data spacemov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下⾯这句操作数的含义是:调⽤地址 = _sys_call_table + %eax * 4。参⻅列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了⼀个包括72 个
;// 系统调⽤C 处理函数的地址数组表。call [_sys_call_table+eax*4]push eax ;// 把系统调⽤号⼊栈。mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下⾯97-100 ⾏查看当前任务的运⾏状态。如果不在就绪状态(state 不等于0)就去执⾏调度程序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执⾏调度程序。cmp dword ptr [state+eax],0 ;// statejne reschedulecmp dword ptr [counter+eax],0 ;// counterje reschedule
;// 以下这段代码执⾏从系统调⽤C 函数返回后,对信号量进⾏识别处理。
ret_from_sys_call:
可是为什么我们⽤的系统调⽤,从来没有⻅过什么 int 0x80 或者 syscall 呢?都是直接调⽤
上层的函数的啊?
这是因为Linux的gnu C标准库,给我们把⼏乎所有的系统调⽤全部封装了。

操作系统不提供系统调用接口,只提供系统调用号,所以我们所谓的系统调用都是经过上层封装之后的接口

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

缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。
操作系统就是躺在中断处理例程上的代码块!
CPU内部的软中断,⽐如int 0x80或者syscall,我们叫做 陷阱
CPU内部的软中断,⽐如除零/野指针等,我们叫做 异常。(所以,能理解“缺⻚异常”为什么这么叫了吗?)
4-4 如何理解内核态和⽤户态

在内存中,对于不同的用户进程,会被分配一份用户页表,而这些进程都会被分配相同的内核页表也就是说操作系统⽆论怎么切换进程,都能找到同⼀个操作系统!换句话说操作系统系统调⽤⽅法的执⾏,是在进程的地址空间中执⾏的!

⽤户态就是执⾏⽤户[0,3]GB时所处的状态
内核态就是执⾏内核[3,4]GB时所处的状态
关于特权级别,涉及到段,段描述符,段选择⼦,DPL,CPL,RPL等概念, ⽽现在芯⽚为了保证兼容性,已经⾮常复杂了,进⽽导致OS也必须得照顾它的复杂性,这块我们不做深究了。

对于当前所处是内核态还是用户态有许多区分方法,在CPU内部有寄存器,区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别,这是一个两位的寄存器,当值为0时,表示当前为内核态,当值为3时,表示当前为用户态。此外,在页表进行申请时,也会对当前状态进行区分。

5. 可重⼊函数

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

函数的局部变量和参数因 “每个调用对应独立栈帧” 而具有隔离性,多个控制流程调用同一函数时,操作的是不同内存区域的数据,因此不会错乱;而全局变量因 “所有调用共享同一块内存”,在重入时容易因并发修改导致状态不一致,这正是不可重入函数的核心问题。

如果⼀个函数符合以下条件之⼀则是不可重⼊的:
调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。

6. volatile

该关键字在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;
}sig:sig.c
gcc -o sig sig.c #-O2
.PHONY:clean
clean:
rm -f sig^Cchage flag 0 to 1
process quit normal
标准情况下,键⼊ CTRL-C ,2号信号被捕捉,执⾏⾃定义动作,修改 flag1 , while 条件不满⾜, 退出循环,进程退出
#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;
}sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1
优化情况下,键⼊ CTRL-C ,2号信号被捕捉,执⾏⾃定义动作,修改 flag1 ,但是 while 条件依旧满⾜,进程继续运⾏!但是很明显flag肯定已经被修改了,但是为何循环依旧执⾏?很明显,while 循环检查的 flag,并不是内存中最新的 flag,这就存在了数据⼆异性的问题。 while 检测的 flag 其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile
#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;
}sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig^Cchage flag 0 to 1
process quit normal
volatile 作⽤:保持内存的可⻅性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进⾏操作

7. SIGCHLD信号

进程⼀章讲过⽤wait和waitpid函数清理僵⼫进程,⽗进程可以阻塞等待⼦进程结束,也可以⾮阻 塞地查询是否有⼦进程结束等待清理(也就是轮询的⽅式)。采⽤第⼀种⽅式,⽗进程阻塞了就不能处理⾃⼰的⼯作了;采⽤第⼆种⽅式,⽗进程在处理⾃⼰的⼯作的同时还要记得时不时地轮询⼀下,程序实现复杂。
其实,⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以⾃定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程 终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。
请编写⼀个程序完成以下功能:⽗进程fork出⼦进程,⼦进程调⽤exit(2)终⽌,⽗进程⾃定义SIGCHLD信号的处理函数, 在其中调⽤wait获得⼦进程的退出状态并打印。
事实上,由于UNIX 的历史原因,要想不产⽣僵⼫进程还有另外⼀种办法:⽗进程调⽤sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不会产⽣僵⼫进程,也不会通知⽗进程。系统默认的忽略动作和⽤⼾⽤sigaction函数⾃定义的忽略 通常是没有区别的,但这是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可⽤。请编写程序验证这样做不会产⽣僵⼫进程。
测试代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{pid_t id;while( (id = waitpid(-1, NULL, WNOHANG)) > 0) {printf("wait child success: %d\n", id);}printf("child is quit! %d\n", getpid());
}int main()
{signal(SIGCHLD, handler);pid_t cid;if((cid = fork()) == 0){//childprintf("child : %d\n", getpid());sleep(3);exit(1);}while(1){printf("father proc is doing some thing!\n");sleep(1);}return 0;
}

今天的内容就是这些,我们下次再见!

http://www.dtcms.com/a/407408.html

相关文章:

  • 门户网站盈利选服务好的佛山网站建设
  • 【开题答辩全过程】以 “物联网医院”-移动护理系统为例,包含答辩的问题和答案
  • 做网站的工作量怎么编辑网站内容
  • 基于STM32单片机的温湿度采集循迹避障APP小车
  • 单片机--概述
  • 文件与内容查找,压缩与解压
  • Emacs折腾日记(三十一)——org mode入门
  • 做网站推广的好处青岛市住房和城乡建设局官方网站
  • 科技网站域名北京顺义网站建设
  • 电子政务建设网站图片十大ppt模板免费下载网站
  • CentOS 7 安装并配置静态网络
  • 如何做网站使用手册济南网站定制策划
  • 什么网站可以做推广的宣传制作清单及价格
  • 龙海网站建设价格商城小程序开发哪家好
  • 厦门汽车充电站建设报备网站深圳浪尖工业设计公司
  • 创意交互设计广东短视频seo搜索哪家好
  • 亚马逊ImageSmith测试:搜索广告从“展示”到“对话”的革命
  • C语言程序设计笔记—scanf、算术运算符的使用案例
  • 旅行社手机网站建设方案wordpress tag 转拼音
  • 网站建设越来越注重用户体验网站开发职业资格证书
  • 基于Rokid平台的AR沉浸式教育导览应用:从构思到实现的全流程研究
  • 网站开发 足球球队信息做静态网站的软件
  • 青岛网站设计公司我国有哪些企业网站
  • 《用AI重构工业设备故障预警系统:从“被动维修”到“主动预判”的协作实践》
  • 建设部考试网站网站开发兼职网站
  • dw建设个人网站步骤wap网站管理系统
  • 出现了oom情况怎么解决
  • JAVA解题——输入年月返回天数(附源代码)
  • 基于仓颉语言BigInt库实现SM2国密算法
  • 安康市城市建设局网站武夷山景区网站建设特点