Linux信号+再谈进程地址空间

Posted 蒋灵瑜的笔记本

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux信号+再谈进程地址空间相关的知识,希望对你有一定的参考价值。


目录

一、Linux中的信号

1、Linux中的信号

2、进程对信号的处理

3、信号的释义

二、信号的捕捉

1、信号的捕捉signal()

2、信号的捕捉sigaction()

三、信号如何产生?

1、kill()用户调用kill向操作系统发送信号

通过命令行参数模仿写一个kill命令

2、raise()进程自己给自己发任意信号(实际上是操作系统->进程)

3、abort()进程自己给自己发6号信号

4、硬件异常产生信号

4.1八号信号SIGFPE(除零错误可引发)

4.2十一号信号SIGSEGV(段错误可引发)

5、软件条件产生异常

5.1十三号信号SIGPIPE(匿名管道读端关闭,写端收到该信号)

5.2十四号信号SIGALRM(定时器)

6、信号相关问答

四、进程退出时的核心转储

1、核心转储的定义

2、核心转储的意义

五、信号的保存(位图结构)

1、相关概念铺垫

2、信号在内核中的表示

六、信号的处理

1、再谈进程地址空间

1.1用户态->内核态

1.2进程如何从用户态切换至内核态并执行内核代码

2、信号的捕捉流程

3、sigset_t信号集(调库,用于处理block和pending位图中的01)

4、sigprocmask(调用该函数可读取或更改阻塞信号集)

5、sigpending(获取当前进程的pending信号集)

6、屏蔽信号并实时打印pending位图(运用上方三个接口)

七、可重入函数

八、volatile关键字

九、SIGCHLD信号


一、Linux中的信号

1、Linux中的信号

使用kill -l查看所有信号。使用信号时,可使用信号编号或它的宏。

1、Linux中信号共有61个,没有0、32、33号信号。

2、【1,31】号信号称为普通信号,【34,64】号信号称为实时信号。

以普通信号为例,进程task_struct结构体中存在unsigned int signal变量用以存放普通信号。(32个比特位中使用0/1存储、区分31个信号——位图结构)

那么发送信号就是修改进程task_struct结构体中的信号位图。当然,有权限改动进程PCB的,也只有操作系统了。

2、进程对信号的处理

1、进程本身是程序员编写的属性和逻辑的集合;

2、信号可以随时产生(异步)。但是进程当前可能正在处理更为重要的事情,当信号到来时,进程不一定会马上处理这个信号;

3、所以进程自身必须要有对信号的保存能力;

4、进程在处理信号时(信号被捕捉),一般有三种动作:默认、自定义、忽略。

3、信号的释义

man 7 signal查看信号详细信息的命令

Trem:正常结束;Core:异常退出,可以使用核心转储功能定位错误,见本文第四节;Ign:内核级忽略。

2)SIGINT  终止信号,即键盘输入ctrl+c
3)SIGQUIT 终止信号,即键盘输入ctrl+\\
6)SIGABRT 终止信号  调用abort即可收到该信号
8)SIGFPE  终止信号  除0错误即可收到该信号
11)SIGSEGV 终止信号 段错误即可收到该信号
13)SIGPIPE 终止信号 匿名管道读端关闭,写端即可收到该信号
14)SIGALRM 终止信号 alarm()函数(定时器)
17)SIGCHLD 内核级忽略信号 子进程退出时会向父进程发送该信号
18)SIGURG 继续进程(进程切换至后台运行,通过9号信号杀掉)
19)SIGSTOP 暂停进程

可以发现,有挺多信号的功能都是一样的。这是因为不同的信号,可以代表发生了不同的事件,但处理结果可以一致。

二、信号的捕捉

1、信号的捕捉signal()

SIGNAL(2) 
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);//signum:被捕捉的信号编号;handler:对指定的信号设置自定义动作
handler设置为SIG_DFL表示信号默认处理方式,SIG_ING设置为忽略处理
#include <iostream>
#include <unistd.h>
#include <signal.h>
void hancler(int signo)

	//这里写自定义内容,捕获到signo信号后即可执行自定义代码
    std::cout<<"进程捕捉到信号"<<signo<<std::endl;

int main()

    signal(2,hancler);//外部需要对该进程发送信号
    while(1)
    
        std::cout<<getpid()<<std::endl;
        sleep(1);
    
    return 0;

外部需要对该进程发送信号,才能被signal接口捕捉。上面例子中,外部发送kill -2 PID或者键盘ctrl+c都行。

当捕捉到指定信号后,将会执行自定义函数。可用于信号功能的替换。

9号和19号信号无法被捕捉kill -9乱杀进程,kill -19暂停进程。

2、信号的捕捉sigaction()

SIGACTION(2)    
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
signum:信号;act:结构体对象;oldact:输出型参数,记录原来的act对象
struct sigaction 
   void (*sa_handler)(int);//回调方法
   void (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t sa_mask;//阻塞信号集
   int sa_flags;
   void (*sa_restorer)(void);//用于支持旧版本的sigaction函数的信号处理函数地址,一般不使用。
;
Sigaction()在成功时返回0; 在错误时返回 -1,并设置 errno。

当一个信号正在被递达执行期间,pending位图由1置0,同时该信号将被阻塞。

如果这时再接收到这个信号,发现该信号被阻塞,同时pending位图由0置1,保存这个信号。

若同一时间再接收到该信号,由于pending已存满,多余的该信号将被丢失。

当首个信号被捕捉完毕,操作系统会立即解除对该信号的屏蔽,因为pending位图对应的比特位是1,所以立即执行新的捕捉动作,同时pending位图该信号位由1清零。

这就是上图执行结果出现两次2号信号捕捉的原因。

三、信号如何产生?

1、kill()用户调用kill向操作系统发送信号

通过命令行参数模仿写一个kill命令

有一个系统调用kill,用户使用kill函数让操作系统向进程发送信号。

KILL(2) 
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);//pid:目标进程的pid。sig:几号信号
成功时(至少发送了一个信号) ,返回零。出现错误时,返回 -1设置errno

mysignal.cc

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <string>
void Usage(const std::string& proc)

    std::cout<<"Usige:"<<getpid()<< "Signno\\n"<<std::endl;


int main(int argc,char* argv[])//运行main函数时,需要先进行传参

    if(argc!=3)//如果传入main函数的参数个数不为3
    
        Usage(argv[0]);
        exit(1);
    
    pid_t pid=atoi(argv[1]);//获取第一个命令行参数,作为pid
    int signo=atoi(argv[2]);//获取第二个命令行参数,作为signo
    int n=kill(pid,signo);//需要发送信号的进程/发送几号信号
    if(n==-1)//kill()失败返回-1
    
        perror("kill");
    
    while(1)
    
        std::cout<<getpid()<<std::endl;
        sleep(1);
    
    return 0;

2、test.cc

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
int main()

    while(1)
    
        std::cout<<"这是一个正在运行的进程"<<getpid()<<std::endl;
        sleep(1);
    
    return 0;

2、raise()进程自己给自己发任意信号(实际上是操作系统->进程)

RAISE(3)
#include <signal.h>
int raise(int sig);//sig:信号编号
raise()在成功时返回0,在失败时返回非0。

raise(signo)等于kill(getpid,signo);

//当计数器运行到5时,进程会因3号进程退出
int main(int argc,char* argv[])//运行main函数时,需要先进行传参

    int cnt=0;
    while(cnt<=10)
    
        std::cout<<cnt++<<std::endl;
        sleep(1);
        if(cnt>=5)
        
            raise(3);
        
    
    return 0;

3、abort()进程自己给自己发6号信号

ABORT(3)
#include <stdlib.h>
void abort(void);
函数 abort()永远不会返回

abort()等于kill(getpid,SIGABRT);

4、硬件异常产生信号

硬件异常指非人为调用系统接口等行为,因软件问题造成的硬件发生异常。操作系统通过获知对应硬件的状态,即可向对应进程发送指定信号。

4.1八号信号SIGFPE(除零错误可引发)

例如出现除0错误,操作系统将会发送8号信号SIGFPE。

此时使用signal()捕捉这个信号,就会发现8号信号一直在被捕捉。这是因为状态寄存器是由CPU进行维护的,当8号信号被捕捉,进程并没有退出,根据时间片轮转,当进程被切换/剥离至CPU时,会读取和保存当前寄存器的上下文信息,所以我们就看到了8号信号被死循环捕捉。

4.2十一号信号SIGSEGV(段错误可引发)

5、软件条件产生异常

5.1十三号信号SIGPIPE(匿名管道读端关闭,写端收到该信号)

例如匿名管道读端关闭,操作系统会向写端发送13号信号SIGPIPE终止写端。

5.2十四号信号SIGALRM(定时器)

设置alarm函数是在告诉操作系统,将在设定的时间到来时,向进程发送14号信号终止进程。

ALARM(2)  
#include <unistd.h>
unsigned int alarm(unsigned int seconds);//seconds延时几秒
返回值为定时器剩余的秒数(可能会被提前唤醒)
alarm(0)表示取消之前设定的闹钟
//设置一个cnt,用于测试代码在指定时间跑了多少
void hancler(int signo)

	//这里写自定义内容,捕获到signo信号后即可执行自定义代码
    std::cout<<"进程捕捉到信号"<<signo<<" "<<cnt<<std::endl;//检测到5秒后cnt为多少
    alarm(5);//循环捕捉闹钟

int main()

    signal(14,hancler);
    alarm(1);//定时1秒
    alarm(5);//定义新的闹钟,旧闹钟会失效哦
    while(1)
    cnt++;
    return 0;

闹钟是由软件实现的。任何一个进程,都可以通过alarm函数设定闹钟,所以操作系统需要通过先描述再组织的方式管理这些闹钟。

6、信号相关问答

所有信号产生,最终都要有操作系统来进行执行,因为操作系统是进程的管理者 。

信号的处理是否是立即处理的?见下文~

信号如果没有被立即处理,那么信号将被保存至pending位图中

一个进程在没有收到信号的时候,能否知道,自己应该对合法信号作何处理呢? 能,程序员写好了对应信号的处理方式(你没走人行道但你知道红灯停,绿灯行)

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?操作系统直接修改进程pcb中的信号位图。

四、进程退出时的核心转储

信号旁边写着Core的信号,都可以使用核心转储功能。

1、核心转储的定义

核心转储:当进程出现异常时,将进程在对应时刻的有效数据由内存存储至磁盘。

云服务器默认关闭了核心转储。在终端输入ulimit -a显示操作系统各项资源上限;使用ulimit -c 1000允许操作系统最大设置1000个block大小的数据块。

2、核心转储的意义

将程序异常的原因转储至磁盘,支持后续调试。

五、信号的保存(位图结构)

1、相关概念铺垫

1、信号递达(Delivery) :实际执行信号的处理动作;

2、信号未决(Pending):信号从产生到递达之间的状态

3、进程可以选择阻塞 (Block )某个信号。

4、信号被阻塞时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

5、阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2、信号在内核中的表示

例如signal捕获信号的流程就是通过signo编号修改handler[signo]中的函数指针指向用户自定义的信号处理方法。当收到信号时,将pending位图中对应的比特位修改为1,若block位图中没有阻塞该信号,该信号被递达时就会执行该信号的处理方法。

对于普通信号,pending位图同时间只能保存一次同个信号,若该信号处于未递达状态,后续再次收到该信号将无法被保存(丢失)。

六、信号的处理

1、再谈进程地址空间

博主首篇进程地址空间传送门:【Linux】进程地址空间

1.1用户态->内核态

1.2进程如何从用户态切换至内核态并执行内核代码

每个进程的虚拟地址空间中有一块1G大小的内核空间,通过内核级页表映射的方式找到物理内存中内核代码进行执行。

由于内核级页表中对应物理地址的映射关系是一样的,所以每个进程都可以使用相同的内核级页表,无论进程如何切换,均可使用同一张内核级页表进行映射调用。

在进行用户态->内核态的切换过程中,首先通过CR3寄存器将进程状态由用户态修改为内核态(陷入内核),在本进程的内核空间中找到物理内存中的内核代码进行执行,执行完毕后将结果返回给进程。

2、信号的捕捉流程

信号的自定义捕捉:信号在产生的时候,不会被立刻处理,而是从内核态返回用户态的时候,对信号进行处理。

进程首先因为中断、异常、系统调用陷入内核,以内核态的身份运行内核代码,通过进程控制块中的信号位图分析当前信号的处理方式。

 若为自定义处理,则需要进程回到用户态去执行用户设定的handler方法。为什么进程不能以内核态的身份直接执行handler方法?这是因为进程处于内核态,权限非常高,操作系统是没有能力识别代码的逻辑的,若handler被人为植入恶意代码,原先部分没有权限的代码因为执行身份的变化而被提权,所以操作系统必须让进程先回到用户态,降低进程的权限。

执行完handler方法后,进程需要重新回到内核态去执行一些系统调用,才能回退回用户态。

3、sigset_t信号集(调库,用于处理block和pending位图中的01)

每个信号只有一个bit的未决/阻塞标志,非0即1,不记录该信号产生了多少次。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集。这个类型可以表示每个信号的“有效”或“无效”状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

#include <signal.h>
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
int sigemptyset(sigset_t *set);
函数sigfifillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。 
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);

在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfifillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

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

4、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参数的可选值。

how:如何屏蔽信号集

SIG_BLOCK

set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask | set

SIG_UNBLOCK

set包含了我们希望从当前信号屏蔽字解除阻塞的信号,相当于mask=mask&~set

SIG_SETMASK

设置当前信号屏蔽字为set所指向的值,相当于mask=set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。没有手动捕捉的话,一般信号都是终止的,所以递达了,进程大概率也就寄了。

5、sigpending(获取当前进程的pending信号集)

SIGPENDING(2)
#include <signal.h>
int sigpending(sigset_t *set);//set:输出型参数,输出当前进程pending位图
sigending()在成功时返回0,在错误时返回-1。在发生错误时,将 errno 设置。

6、屏蔽信号并实时打印pending位图(运用上方三个接口)

默认情况所有的信号是不被阻塞的,如果一个信号被屏蔽了,那么这个信号不会被递达。

#include <iostream>
#include <vector>
#include <signal.h>
#include <unistd.h>

// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31

using namespace std;

// static vector<int> sigarr = 2,3;
static vector<int> sigarr = 2;

static void show_pending(const sigset_t &pending)

    for(int signo = MAX_SIGNUM; signo >= 1; signo--)
    
        if(sigismember(&pending, signo))
        
            cout << "1";
        
        else cout << "0";
    
    cout << "\\n";


static void myhandler(int signo)

    cout << signo << " 号信号已经被递达!!" << endl;


int main()

    for(const auto &sig : sigarr) signal(sig, myhandler);

    // 1. 先尝试屏蔽指定的信号
    sigset_t block, oblock, pending;
    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    // 1.2 添加要屏蔽的信号
    for(const auto &sig : sigarr) sigaddset(&block, sig);
    // 1.3 开始屏蔽,设置进内核(进程)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2. 遍历打印pengding信号集
    int cnt = 10;
    while(true)
    
        // 2.1 初始化
        sigemptyset(&pending);
        // 2.2 获取它
        sigpending(&pending);
        // 2.3 打印它
        show_pending(pending);
        // 3. 慢一点
        sleep(1);
        if(cnt-- == 0)
        
            sigprocmask(SIG_SETMASK, &oblock, &block); // 一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号!
            cout << "恢复对信号的屏蔽,不屏蔽任何信号\\n";
        
    

七、可重入函数

main函数调用insert函数向一个链表head中插入节点P1,插入操作分为两步,刚执行完第一句代码,此时硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作执行完毕后,sighandler返回内核态,再次回到用户态就从main函数继续执行刚才剩余的代码。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有P1真正插入链表中,P2这个节点谁都找不到了。发生内存泄漏

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱。像这样的函数称为不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

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

八、volatile关键字

优化后,通过信号自定义方法handler修改全局q,但是程序不会退出。

O3优化时:编译器认为q在main执行流中没有被修改,所以编译器对q做了优化,直接将q放在了寄存器中,这样后续执行时就不用再去内存中读取q了,提高了程序运行效率。虽然handler中修改了内存中的q,但是寄存器中的q值一直是1(寄存器中的q值是临时值,操作系统没有对其进行修改),所以会发生上图效果。

解决方法:给q加volatile关键字,让q通过内存读取而不是寄存器,保持变量q的内存可见性。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
volatile int q=1;//保持内存可见性
void handler(int signo)

    q=0;

int main()

    signal(2,handler);
    while(q!=0);
    return 0;

当程序结果与预期偏离时,可以尝试使用volatile关键字,万一就是编译器过度优化造成的程序逻辑异常呢?

九、SIGCHLD信号

1、子进程退出,会向父进程发送17号信号SIGCHLD;

2、由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

//忽略子进程发出的17号信号
signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);//act中忽略17号信号

系统默认的忽略动作和用户用signal/sigaction函数自定义的忽略 通常是没有区别的,但这里是一个特例。

虽然信号SIGCHID的默认动作也是忽略,但这个忽略是实实在在的无视了这个信号;我们手动在handler方法中使用SIG_IGN,子进程退出时发送给父进程的信号将会被父进程忽略,但子进程会被操作系统回收,这就是区别所在。

linux进程管理

1.进程的组成

一个进程包含内核中的一部分地址空间和一系列数据结构。其中地址空间是内核标记的一部分内存以供进程使用,而数据结构则用来纪录每个进程的具体信息。

最主要的进程信息包括:

  • 进程的地址空间图
  • 进程当前的状态( sleeping、stopped、runnable 等)
  • 进程的执行优先级
  • 进程调用的资源信息
  • 进程打开的文件和网络端口信息
  • 进程的信号掩码(指明哪种信号被屏蔽)
  • 进程的属主

PID:进程ID

每一个进程都会从内核获取一个唯一的ID值。绝大多数用来操做京城的命令和系统调用,都需要PID

指定操作的进程对象。

PPID:父进程ID

在unix和linux系统中,一个已经存在的进程必须克隆它自身来创建一个新的进程。当新的进程克隆后,最初的进程便作为父进程存在。

UID&EUID:真实用户ID和有效用户ID

一个进程的 UID 是其创建者的身份标志(也是对其父进程 UID 的复制)。通常只有进程的创建者和超级用户才有操作该进程的权限。

Niceness

一个进程的计划优先级决定了它能获取到的 CPU 时间。内核有一个动态的算法来计算优先级,同时也会关注一个 Niceness 值,来决定程序运行的优先顺序。

二.信号

信号属于进程级别的中断请求。它们可以作为进程间通信的手段,或者由终端发送以杀死,中断,挂起某个进程。

 

 

三。kill命令

kill命令常用来终止某个进程,它可以向进程传递信号(默认为TERM)

不带任何数字(信号)选项的kill命令1并不能保证进程被杀死,因为kill命令默认发送term信号,而term是可以被捕获,屏蔽或忽略的。

可以使用kill -9 pid命令强制杀死进程(9代表KILL信号,不可被捕获,屏蔽或者忽略)。

KILL命令需要指定进程的PID号

pgrep 命令可以通过程序名称(或其他属性如 UID)筛选进程号,pkill 命令可以直接发送指定信号给筛选结果。
如 sudo pkill -u ben
该命令将发送 TERM 信号给所有属于用户 ben 的进程。

 

 

killall 命令可以通过程序名称杀死指定进程的所有实例。如:

sudo killall apache2

 

 

 进程管理命令

  pstree

    通过树形结构显示进程关系

    yum install -y psmisc -y   安装命令

 

  ps

    查看进程状态--当前运行状态;(这一刻)

    PID进程ID

    TTY启动该进程的终端是谁  pts

    TIME进程累计再CPU上运行的时长

    CMD启动该进程命令程序

  ps的常用组合:

  ps -aux

    USER 进程所属用户

    PID

    

    %CPU
    %MEM
    VSZ virtual memory size 虚拟内存大小;进程自己再内存中真是占用的线性地址空间大小
  RSS 常驻内空间(不可以进行内存交换 -- swap)
    TTY

    STAT

 

 

top命令

  显示进程信息     top

  显示完整命令    top -c

  以批处理模式显示程序信息     top -b

  以累积模式显示程序信息    top  -s

  设置信息更新时间    top -d  3

  显示指定进程信息  top  -p  139     //显示进程号为139的进程信息,cpu

,内存占用率

  显示更新十次后退出   top  -n  10

  使用者将不能利用交谈式指令来对行程下命令  top -s

 

 

 

        

  

 

以上是关于Linux信号+再谈进程地址空间的主要内容,如果未能解决你的问题,请参考以下文章

Linux 面试

Linux多任务编程——线程

十八Linux 进程与信号---进程介绍

Linux——随笔

linux多线程概念详述

(转载)linux下的僵尸进程处理SIGCHLD信号Linux环境进程间通信: 共享内存(下)