本节重点:

  1. 掌握Linux信号的基本概念。

  2. 掌握信号产生的一般方式。

  3. 理解信号递达和阻塞的概念,原理。

  4. 掌握信号捕捉的一般方式。

  5. 了解中断过程,理解中断的意义

  6. 掌握操作系统运行,系统调用原理,理解缺页异常或其他软件异常的基本原理

  7. 重新了解可重入函数的概念。

  8. 了解竞态条件的情景和处理方式。

  9. 了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制。

1. 信号快速认识

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 一个系统函数

NAME 
    signal - 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 信号概念

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

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

#define SIGINT 2

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件

下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal


1.3.1 信号处理

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

• 忽略此信号

#include <iostream> 
1#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)一个信号。

// 就是开始的样例

注意看源码:

#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. 产生信号

当前阶段:

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

2.1.2 理解OS如何得知键盘有数据

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 -11213784 , 11 是信号 SIGSEGV 的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本⾝没错,给它发SIGSEGV也能产生段错误。


2.3 使用函数产生信号

2.3.1 kill

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

NAME 
        kill - send signal to a process 
SYNOPSIS 
        #include <sys/types.h> 
        #include <signal.h> 
        
        int kill(pid_t pid, int sig); 
        
RETURN VALUE 
        On 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 函数可以给当前进程发送指定的信号(自己给自己发信号)。

 NAME 
        raise - send a signal to the caller 
        
SYNOPSIS 
        #include <signal.h> 
        int raise(int sig); 
        
RETURN VALUE 
        raise() 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.2 abort

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

NAME 
        abort - cause abnormal process termination 
        
SYNOPSIS 
        #include <stdlib.h> 
        
        void abort(void); 
RETURN VALUE 
        The abort() function never returns. 
        // 就像exit函数一样,abort函数总是会成功的,所以没有返回值。 

#include <iostream> 
1#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 信号。

NAME 
        alarm - set an alarm clock for delivery of a signal 
        
SYNOPSIS 
        #include <unistd.h> 
        
        unsigned int alarm(unsigned int seconds); 
        
RETURN VALUE 
        alarm() returns the number of seconds remaining until any previously 
        scheduled 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 
whb@bite:~/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++; 
    } 
} 
NAME 
    pause - wait for signal 
    
SYNOPSIS 
    #include <unistd.h> 
    
    int pause(void); 
    
DESCRIPTION 
        pause() causes the calling process (or thread) to sleep until a signal 
        is delivered that either terminates the process or causes the invoca‐ 
        tion of a signal-catching function. 
    
RETURN VALUE 
        pause() returns only when a signal was caught and the signal-catching 
        function returned. In this case, pause() returns -1, and errno is set 
        to EINTR.
// 窗口 1 
$ ./alarm 
我的进程pid是: 216982 
剩余时间 : 13 // 提前唤醒它,剩余时间 
剩余时间 : 0 
剩余时间 : 0 
剩余时间 : 0 

// 窗口 2 
$ kill -14 216982 

📌 结论:

• 闹钟设置一次,起效一次• 重复设置的方法

• 如果时间允许,可以测试一下 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

#include <stdio.h> 
#include <signal.h> 

void handler(int sig) 
{ 
    printf("catch a sig : %d\n", sig); 
} 

// v1 
int main() 
{ 
    //signal(SIGFPE, handler); // 8) SIGFPE 
    sleep(1); 
    int a = 10; 
    a/=0; 
    
    while(1); 
    return 0; 
} 

2.5.2 模拟野指针

//默认行为 
[hb@localhost code_test]$ cat sig.c 
#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; 
} 

[hb@localhost code_test]$ ./sig 
Segmentation fault (core dumped) 
[hb@localhost code_test]$ 

//捕捉行为 
[hb@localhost code_test]$ cat sig.c 
#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; 
} 

[hb@localhost code_test]$ ./sig 
[hb@localhost code_test]$ ./sig 
catch a sig : 11 
catch a sig : 11 
catch a sig : 11 

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

📌 注意:

通过上面的实验,我们可能发现:

发现一直有8号信号产生被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何

处理后续的流程,实际上 OS 会检查应用程序的异常情况,其实在CPU中有一些控制和状态

寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解

为一个位图,对应着一些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存

在就会调用对应的异常处理方法。

除零异常后,我们并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以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; 
} 

$ man 7 signal 
SIGABRT         P1990         Core     Abort signal from abort(3) 
SIGALRM         P1990         Term     Timer signal from alarm(2) 
SIGBUS          P2001         Core     Bus error (bad memory access) 
SIGCHLD         P1990         Ign      Child stopped or terminated 
SIGCLD            -           Ign      A synonym for SIGCHLD 
SIGCONT         P1990         Cont     Continue if stopped 
SIGEMT            -           Term     Emulator trap 
SIGFPE         P1990          Core     Floating-point exception 
SIGHUP         P1990          Term     Hangup detected on controlling terminal 
                                       or death of controlling process 
SIGILL         P1990          Core     Illegal Instruction 
SIGINFO          -                     A synonym for SIGPWR 
SIGINT         P1990          Term     Interrupt from keyboard 
SIGIO            -            Term     I/O now possible (4.2BSD) 
SIGIOT           -            Core     IOT trap. A synonym for SIGABRT 
SIGKILL        P1990          Term     Kill signal 
SIGLOST          -            Term     File lock lost (unused) 
SIGPIPE        P1990          Term     Broken pipe: write to pipe with no readers; see pipe(7) 
SIGPOLL        P2001          Term     Pollable event (Sys V). 

$ ulimit -a 
core file size         (blocks, -c) 0 
data seg size          (kbytes, -d) unlimited 
scheduling priority            (-e) 0 
file size              (blocks, -f) unlimited 
pending signals                (-i) 7643 
max locked memory      (kbytes, -l) 65536 
max memory size        (kbytes, -m) unlimited 
open files                     (-n) 65535 
pipe size           (512 bytes, -p) 8 
POSIX message queues    (bytes, -q) 819200 
real-time priority             (-r) 0 
stack size             (kbytes, -s) 8192 
cpu time              (seconds, -t) unlimited 
max user processes             (-u) 7643 
virtual memory         (kbytes, -v) unlimited 
file locks                     (-x) unlimited 

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

$ ulimit -c 1024 
$ ulimit -a 
core file size             (blocks, -c) 1024 
data seg size              (kbytes, -d) unlimited 
scheduling priority                (-e) 0 
file size                  (blocks, -f) unlimited 
pending signals                    (-i) 7643 
max locked memory          (kbytes, -l) 65536 
max memory size            (kbytes, -m) unlimited 
open files                         (-n) 65535 
pipe size               (512 bytes, -p) 8 
POSIX message queues        (bytes, -q) 819200 
real-time priority                 (-r) 0 
stack size                 (kbytes, -s) 8192 
cpu time                  (seconds, -t) unlimited 
max user processes                 (-u) 7643 
virtual memory             (kbytes, -v) unlimited 
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向进程发送信号?能否描述一下完整的发送处理过程?

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐