【Linux】一篇文章搞定 进程间通信 之 信号机制

1. 信号的概念

信号的概念理解:

  • 信号是一个程序中断

信号的种类:使用Kill -l 命令可以查看有多少个信号

[gongruiyang@localhost TestSignal]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX
  • 1 至 31 是 非可靠信号
  • 34 至 64 是 可靠信号
  • 非可靠信号 : 当前信号有可能丢失的,丢失就无法执行该信号
  • 可靠信号:当前信号不可能会丢失

查看信号的具体含义命令:

man 7 signal

在这里插入图片描述

2. 信号的产生

2.1 硬件产生的信号

#include <stdio.h>
#include <unistd.h>

int main()
{
    
    

  while(1)
  {
    
    
    printf("while循环ing\n");  
    sleep(1);
  }

  return 0;
}

2.3.1 终止进程组合键 ctrl + c

while循环ing
while循环ing
while循环ing
while循环ing
while循环ing
^C
  • 在程序运行过程中我们按下 ctrl + c 就可以中断进程运行
  • 组合键ctrl + c 本质上是SIGINT 信号(2号信号),是一个终止信号,终止正在进行的这个前台进程,该组合键对后台进程没有任何作用

2.3.2 暂停进程组合键 ctrl + z

[gongruiyang@localhost signalCreate]$ ./hardTestExe 
while循环ing
while循环ing
while循环ing
while循环ing
^Z
[1]+  Stopped               ./hardTestExe
[gongruiyang@localhost signalCreate]$ ps aux | grep ./hardTest
gongrui+  10153  0.0  0.0   4216   348 pts/0    T    13:40   0:00 ./hardTestExe
gongrui+  10157  0.0  0.0 112828   992 pts/0    R+   13:40   0:00 grep --color=auto ./hardTest
  • 在程序运行过程中我们按下 ctrl + z 就可以暂停进程运行,此时该进程的进程状态是T,意为暂停状态

  • 组合键 ctrl + z 本质上是SIGTSTP信号(20号信号),是一个暂停信号,让正在运行的前台程序暂停运行

2.3.3 产生核心转储文件组合键 ctrl + |

[gongruiyang@localhost signalCreate]$ ./hardTestExe 
while循环ing
while循环ing
while循环ing
^\Quit(core dumped)
[gongruiyang@localhost signalCreate]$ ls
core.10925  hardTest.c  hardTestExe
  • 在程序运行过程中我们按下 ctrl + | 就可以退出运行的进程,并产生一个core.XXX的核心转储文件

  • 组合键 ctrl + | 本质上是SIGQUIT信号(3号信号),是一个结束进程并产生核心转储文件 的信号

2.3.3.1 核心转储

  1. 核心转储文件概念:核心转储文件中存储的是异常终止进程产生的一个文件,进程终止瞬间将进程地址空间的内容以及有关进程状态的其他信息写出的这个磁盘文件,其中信息常用于调试寻找错误原因
  2. 核心转储概念:在UNIX系统中, 核心映像(core image) 就是进程(process)执行时的内存内容。当进程发生错误或收到“信号”(signal) 而终止执行时,系统会将核心映像写入一个文件,以作为调试之用,这就是所谓的核心转储(core dump)。

2.3.3.2 core file size设置

当进程异常退出或收到信号退出时,却没有产生核心转储文件,此时可以通以下命令查看core file size设置情况,根据打印出来的内容可以看出core file size被设置为0,我们需要修改该设置值为unlimited后,进程异常退出后才能产生核心转储文件

[gongruiyang@localhost signalCreate]$ 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) 14950
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
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) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

修改方式如下:

[gongruiyang@localhost signalCreate]$ ulimit -c unlimited
[gongruiyang@localhost signalCreate]$ ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 14950
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
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) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

2.3.3.3 使用gdb调试核心转储文件

#include <stdio.h>
void func()
{
    
    
  int* p = NULL;
  *p = 10;    //访问空指针导致程序崩溃
}                              
int main()                     
{
    
                                  
  func();                      
                               
  return 0;                           
}                              
  • 上述程序,运行至第5行会访问空指针,导致程序崩溃,进程异常退出,并产生一个核心转储文件
[gongruiyang@localhost signalCreate]$ gcc corefiletest.c -g -o coretest
[gongruiyang@localhost signalCreate]$ ./coretest 
段错误(吐核)
[gongruiyang@localhost signalCreate]$ ls
core.58181  corefiletest.c  coretest  hardTest.c  hardTestExe

gdb调试核心转储文件寻找错误地方

[gongruiyang@localhost signalCreate]$ gdb [可执行文件名] [核心转储文件]
  • 使用gdb 可执行文件 核心转储文件 进入调试界面

在这里插入图片描述

  • 由调试信息可知:在func函数中由于11号信号导致程序终止,问题出现在func函数的第五行
(gdb) bt
#0  0x00000000004004fd in func () at corefiletest.c:5
#1  0x0000000000400513 in main () at corefiletest.c:9
(gdb) f 0
#0  0x00000000004004fd in func () at corefiletest.c:5
5	  *p = 10;    //访问空指针导致程序崩溃
(gdb) p p
$1 = (int *) 0x0
  • 由打印的信息可知 p变量中保存的地址为NULL,所以崩溃原因是对空指针进行了访问

2.3.3.4 一些非法行为对应产生的信号量

非法行为 信号量 信号名
解引用空指针 11号信号并产生核心转储文件 SIGSEGV
访问越界 11号信号并产生核心转储文件 SIGSEGV
动态分配空间free两次 6号信号并产生核心转储文件 SIGABRT

2.3 软件产生的信号

2.3.1 kill

2.3.1.1 kill函数

int kill(pid_t pid, int sig);

功能:

头文件:

  • sys/types.h
  • signal.h

参数:

  • pid : 进程标识符,给哪一个进程发送信号
  • sig : 信号值,具体发送哪一个信号

返回值:

  • 成功:返回信号值
  • 失败:返回 -1

测试程序:测试kill函数

#include <stdio.h>  
#include <sys/types.h>  
#include <signal.h>  
#include <unistd.h>  
int main()  
{
    
      
  int ret_kill = kill(getpid(),2);  
  if(ret_kill == -1)  
    perror("kill");                   
  else  
    printf("信号量:%d\n",ret_kill);  
                         
  return 0;              
}                        
[gongruiyang@localhost signalinterface]$ ./killTest 

[gongruiyang@localhost signalinterface]$ 

2.3.1.2 kill命令

kill命令可以指定给具体进程发送具体信号量

kill -signal pid
  • signal:信号量,可以是具体数值,也可以是信号名字
  • pid : 进程标识符

通过以下命令获取进程pid

ps aux | grep 进程名
[gongruiyang@localhost TestSignal]$ ps aux |grep deadCircle
gongrui+  59789  0.0  0.0   4216   352 pts/0    S+   16:38   0:00 ./deadCircle
gongrui+  59797  0.0  0.0 112828   984 pts/1    R+   16:39   0:00 grep --color=auto deadC

通过kill -2干掉该进程,也可以使用9号信号量,该信号量是强杀信号,可以干掉大部分进程

[gongruiyang@localhost TestSignal]$ kill -2 59789

2.3.2 abort

2.3.2.1 abort函数

void abort(void);

功能:可以向进程发送SIGABRT信号(6号信号),使进程异常终止,并关闭刷新进程打开的流

头文件:

  • stdlib.h

哪一个进程调用该函数,便向该进程传送SIGABRT信号(6号信号)

其实abort内部封装了kill函数

3. 信号的注册

3.1 进程中的未决信号集(位图)

进程的task_struct中定义了位图的初始定义

struct task_struct {
    
    
    ...
      struct sigpending pending;  
    ...
}

内核源码的include\linux\signal.h中sigpending

struct sigpending {
    
    
	struct list_head list;
	sigset_t signal;
};

内核源码的 include\asm-generic\signal.h中定义了sigset_t

typedef struct {
    
    
	unsigned long sig[_NSIG_WORDS];
} sigset_t;

内核源码的 include\asm-generic\signal.h中定义了**_NSIG_WORDS**

#define _NSIG		64
#define _NSIG_BPW	__BITS_PER_LONG
#define _NSIG_WORDS	(_NSIG / _NSIG_BPW)

内核源码的 arch\alpha\include\asm\bitsperlong.h中定义了**__BITS_PER_LONG**

#define __BITS_PER_LONG 64
  • 一番检查源码之后发现位图就是一个unsigned long sig[1]

  • Linux操作系统中long占8个字节,即64位

  • 每一个信号,在该位图中存在一个与之对应的比特位

  • 当信号对应的比特位为1时,表示当前进程接收到该信号

3.1.1 图解进程与未决信号集的关系

在这里插入图片描述

3.2 非可靠信号注册

前提:当前进程收到了一个非可靠信号

  1. 将当前进程的位图中的对应非可靠信号比特位变成1
  2. 添加sigqueue节点到sigqueue队列当中(如果该信号的sigqueue节点已经存在于sigqueue队列中,则不添加

在这里插入图片描述

3.3 可靠信号注册

前提:当前进程收到一个可靠信号

  1. 将当前进程的位图中的对应可靠信号的比特位变成1
  2. 添加sigqueue节点到sigqueue队列当中(无论该信号的sigqueue节点是否存在于sigqueue队列中,都要添加)

在这里插入图片描述

3.4 sigqueue源码

struct sigqueue {
    
    
	struct list_head list;
	int flags;
	siginfo_t info;
	struct user_struct *user;
};

4. 信号的注销

4.1 非可靠信号的注销

前提:信号已经处理完

  1. 将处理完的信号对应位图中的比特位从1变成0
  2. 将该信号的sigqueue节点从sigqueue队列中出队

4.2 可靠信号的注销

前提:信号已经处理完了

  1. 将该信号的sigqueue节点从sigqueue队列中出队
  2. 需要判断sigqueue队列中是否还有与出队的该信号相同的sigqueue节点
    • 还有相同的sigqueue节点:不修改位图中的对应比特位
    • 没有了:将该信号对应位图中的比特位从1变成0

5. 信号的捕捉后的处理方式

#define SIG_DFL	((__sighandler_t)0)	/* default signal handling */
#define SIG_IGN	((__sighandler_t)1)	/* ignore signal */
#define SIG_ERR	((__sighandler_t)-1)	/* error return from signal */

5.1 默认处理方式SIG_DFL

#define SIG_DFL	((__sighandler_t)0)	/* default signal handling */
  • SIG_DFL就是 __sighandler_t结构体类型 的0

5.2 忽略处理方式SIG_IGN

典型的忽略处理方式:

僵尸进程的产生原因:子进程先于父进程退出,子进程向父进程发送了一个SIGCHLD信号,父进程接收到了该SIGCHLD信号,但是选择了忽略处理的方式,导致了子进程的退出资源未被父进程进行回收,进而导致子进程变成了僵尸进程

#define SIG_IGN	((__sighandler_t)1)	/* ignore signal */
  • SIG_IGN就是 __sighandler_t结构体类型 的1

5.3 自定义信号处理方式

5.3.1 signal函数

自定义 信号处理方式 函数:程序员定义一个函数 去处理接收到的信号

typedef void (*sighandler_t)(int);		// void handler(int)
sighandler_t signal(int signum, sighandler_t handler);

功能:当进程接收到了signum信号时,调用handler函数,执行handler函数中的代码,该信号以前需要执行的任务不再执行

头文件:

  • signal.h

参数:

  • signum : 信号量值,要处理的信号
  • handler : 信号处理句柄,就是一个函数指针,当进程接收到了signum这个信号时,进程需要调用handler函数去做一些事先规定好的事情

5.3.1.1 signal函数演示程序

#include <stdio.h>
#include <signal.h>
#include <unistd.h>          
void handler(int signum)     
{
    
                                
  printf("接收到了2信号!\n");
}                            
int main()                   
{
    
                                
  signal(2,handler);         
  sleep(10);      
    
  return 0;                  
}                            
[gongruiyang@localhost signalinterface]$ ./test 
^C接收到了2信号!

在Main进程在sleep时,按下ctrl + c组合键,向前台进程main发送了一个2号信号,此时Main函数调用Handler函数,进行对信号的处理

5.3.2 sigaciotn函数

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

功能:自定义 信号处理方式 函数:程序员定义一个函数 去处理接收到的信号

头文件:

  • signal.h

参数:

  • signum : 信号量值
  • act : 输入型参数,保存 对signum信号 所采取的措施信息
  • oldact : 输出型参数,保存 以前对signum信号 所采取的措施信息

5.3.2.1 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:函数指针,保存了内核对信号的处理方式:默认处理方式忽略处理方式
  • sa_sigaction:函数指针,保存的时自定义处理函数
  • sa_mask:信号集位图,保存收到的信号
  • sa_flags:填入宏
含义
SA_SIGINFO 操作系统在处理信号的时候,调用的就是sa_sigaction函数指针当中保存的函数
0 操作系统在处理信号的时候,调用的就是sa_handler函数指针当中保存的函数

5.3.2.2 sigaction函数程序演示

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

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

int main()
{
    
    
  struct sigaction act;
  sigemptyset(&(act.sa_mask));	//用于将位图全部位 置为0
  act.sa_flags = 0;	//0表示 使用自定义函数
  act.sa_handler = handler;	//填写 自定义函数

  sigaction(2,&act,NULL);

  while(1)
  {
    
    
    printf("Hello World!\n");
    sleep(1);
  }

  return 0;
}
[gongruiyang@localhost signalinterface]$ ./sigActionExe 
Hello World!
Hello World!
Hello World!
^Csignum : 2
Hello World!
Hello World!
Hello World!
^Csignum : 2
Hello World!
^Csignum : 2
Hello World!
Hello World!
Hello World!
  • 程序解释:2号信号本来执行的是将中断前台进程,但是通过sigaction函数将2号信号的信号处理方式修改成了handler函数中的执行命令,所以在程序运行中,按下ctrl + c组合键不会将前台程序进行中断,只会执行handler函数

5.3.2.3 内核源码中的sigaction结构体源码

struct sigaction {
    
    
	union {
    
    
	  __sighandler_t	_sa_handler;
	  void (*_sa_sigaction)(int, struct siginfo *, void *);
	} _u;
	sigset_t	sa_mask;
	int		sa_flags;
};
#define sa_handler	_u._sa_handler
#define sa_sigaction	_u._sa_sigaction

typedef char* __user __sighandler_t;
typedef struct {
    
    
	unsigned long sig[_NSIG_WORDS];
} sigset_t;

5.3.3 signal函数与sigaction函数的关系

  • signal函数只是修改了sigaction结构体中的_sa_sigaction
  • sigaction函数修改了整个sigaction结构体
  • signal函数内部调用了sigaction函数

6. 探究回调函数机制的源码定义

6.1 从task_struct到回调函数handler的结构体嵌套源码

  • 在task_struct中找到sighand_struct结构体指针
task_struct{
    
    
	...
	struct sighand_struct *sighand;
	...
}
  • sighand_struct结构体定义
struct sighand_struct {
    
    
	...
	struct k_sigaction	action[_NSIG];
	...
};
  • k_sigaction结构体定义
struct k_sigaction {
    
    
	struct sigaction sa;
    ...
};
  • sigaction结构体定义
struct sigaction {
    
    
	union {
    
    
	  __sighandler_t	_sa_handler;
	  void (*_sa_sigaction)(int, struct siginfo *, void *);
	} _u;
	sigset_t	sa_mask;
	int		sa_flags;
};
#define sa_handler	_u._sa_handler
#define sa_sigaction	_u._sa_sigaction

#ifdef CONFIG_64BIT
/* function pointers on 64-bit parisc are pointers to little structs and the
 * compiler doesn't support code which changes or tests the address of
 * the function in the little struct.  This is really ugly -PB
 */
typedef char __user *__sighandler_t; 
#else
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;
#endif	

6.1 图解结构体嵌套关系

在这里插入图片描述

6.2 源码中__sighandler_t的解析

#ifdef CONFIG_64BIT
/* function pointers on 64-bit parisc are pointers to little structs and the
 * compiler doesn't support code which changes or tests the address of
 * the function in the little struct.  This is really ugly -PB
 */
typedef char __user *__sighandler_t; 
#else
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;
#endif	
  • 由注释可以看出:__sighandler_t是一个函数指针(function pointer),并且很难看出来,十分的ugly

刚开始没有注意看这个注释,博主就想char*咋能是函数指针呢?源码翻来覆去苦苦寻找__sighandler_t是否还有其它的宏定义,后来仔细读了一下源码注释,才豁然开朗,TMD,绝了,十分ugly

7. 信号的捕捉流程

在这里插入图片描述

  • main函数调用了一个系统调用函数或者调用**库函数(库函数底层也是封装的系统调用函数)**后,cpu从用户态切换到内核态
  • 内核态调用系统调用后想回到用户态需要调用do_signal函数
  • do_signal函数的功能是检查进程是否接受到信号:
    • 如果接受到信号,调用sigcb函数去处理信号(1**.默认处理方式则不需要切换到用户态,直接在内核态进行信号处理;2.自定义处理方式需要切换到用户态**进行信号处理),信号处理完毕后调用sig_return函数表明信号处理完,再调用do_signal函数检查是否接受到新的信号
    • 如果未接收到信号,直接调用sys_return函数,让cpu从内核态切换到用户态

8. 信号阻塞

8.1 信号阻塞概念及内核源码定义

task_struct源码中定义了信号阻塞位图和信号注册位图

struct task_struct{
    
    
	...
	sigset_t blocked, real_blocked;	// 信号阻塞位图
	struct sigpending pending;	// 信号注册位图 位于这个结构体内部
	...
}
  • 当信号阻塞位图block中对应信号的位为1,表示当前进程阻塞该信号
  • 当进程进入内核状态,准备返回到用户态时,调用do_signal函数时,接收到了一个信号,如果该信号的阻塞位图中的对应位置为1,则不会立即去处理该信号
  • 等到该信号的阻塞位图上的该信号对应位变成1之后才会去处理该信号,可靠信号发送了几次处理几次,非可靠信号发送大于等于1次都是处理1次

8.2 设置阻塞位图函数

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能:设置阻塞位图

头文件:

  • signal.h

参数:

  • how : 告诉函数应该做什么,填入宏
含义
SIG_BLOCK 设置某个信号为阻塞
SIG_UNBLOCK 解除对某个信号的阻塞
SIG_SETMASK 替换阻塞位图
  • set : 新替换入的阻塞位图,可以为NULL
  • alodset : 原阻塞位图,可以为NULL

SIG_BLOCK设置阻塞原理:按位或,便将新的要阻塞的信号加入了阻塞位图中 (原阻塞位图 | 新阻塞位图)

SIG_UNBLOCK解除阻塞原理:按位与,便将要解除阻塞的信号的比特位变成0(原阻塞位图 & 新阻塞位图)

8.2.1 程序演示

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
    
    
  printf("signum : %d\n",signum);    
}
int main()
{
    
    
  signal(2,handler);
  signal(40,handler);

  sigset_t set;
  sigfillset(&set);	// 位图全部置为1

  sigprocmask(SIG_SETMASK,&set,NULL);
  while(1)
  {
    
    
    printf("Hello!\n");
    sleep(1);
  }

  return 0;
}

该程序运行起来后,该进程阻塞了所有信号除了 kill -9 其他信号都没用

猜你喜欢

转载自blog.csdn.net/weixin_45437022/article/details/112241617