前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >异步通信之 信号

异步通信之 信号

作者头像
看、未来
发布2021-10-09 11:58:42
1.1K0
发布2021-10-09 11:58:42
举报
请添加图片描述
请添加图片描述

希望打开此篇能对你有帮助。

文章目录

什么是信号?

在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

信号实际上是由内核发送,内核来处理收到的信号。收到信号的进程,必须对信号做出处理(忽略,捕获,默认动作都行)


信号状态

代码语言:javascript
复制
产生
递达——信号到达并且处理完
未决——信号被阻塞

信号的默认处理方式

代码语言:javascript
复制
忽略
默认动作
捕获

注意,9号、19号信号不能捕捉,不能忽略,不能阻塞。


一个完整信号周期

一个完整的信号周期包括这部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:

在这里插入图片描述
在这里插入图片描述

Linux 可使用命令:kill -l(“l” 为字母),查看相应的信号。

在这里插入图片描述
在这里插入图片描述

列表中,编号为 1 ~ 31的信号为传统 UNIX 支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。非可靠信号一般都有确定的用途及含义, 可靠信号则可以让用户自定义使用。


信号的产生方式

代码语言:javascript
复制
1、按键产生
2、硬件异常
3、软件异常
4、kill()函数
5、kill命令

发送信号

代码语言:javascript
复制
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, intsignum);
功能:
    给指定进程发送信号。
    注意:使用 kill() 函数发送信号,接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者是超级用户。

参数:
    pid: 取值有 4 种情况:
    pid > 0: 将信号传送给进程 ID 为pid的进程。
    pid = 0: 将信号传送给当前进程所在进程组中的所有进程。
    pid = -1: 将信号传送给系统内所有的进程。
    pid < -1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。

    signum: 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令kill -l 进行相应查看。

返回值:
    成功:0
    失败:-1
代码语言:javascript
复制
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <signal.h>  
  
int main(int argc, char *argv[])  
{  
    pid_t pid;  
    int i = 0;  
  
    pid = fork(); // 创建进程  
    if( pid < 0 ){ // 出错  
        perror("fork");  
    }  
      
    if(pid == 0){ // 子进程  
        while(1){  
            printf("I am son\n");  
            sleep(1);  
        }  
    }else if(pid > 0){ // 父进程  
        while(1){  
            printf("I am father\n");  
            sleep(1);  
              
            i++;  
            if(3 == i){// 3秒后  
                kill(pid, SIGINT); // 给子进程 pid ,发送中断信号 SIGINT  
                // kill(pid, 2); // 等级于kill(pid, SIGINT);  
            }  
        }  
    }  
  
    return 0;  
}  

等待信号

代码语言:javascript
复制
    #include <unistd.h>
    int pause(void);
    功能:
        等待信号的到来(此函数会阻塞)。将调用进程挂起直至捕捉到信号为止,此函数通常用于判断信号是否已到。

    返回值:
        直到捕获到信号才返回 -1,且 errno 被设置成 EINTR。
代码语言:javascript
复制
#include <unistd.h>  
#include <stdio.h>  
  
int main(int argc, char *argv[])  
{  
    printf("in pause function\n");  
    pause();  
      
    return 0;  
}  

处理信号

代码语言:javascript
复制
#include <signal.h>

typedef void (*sighandler_t)(int);// 回调函数的声明

sighandler_t signal(int signum,sighandler_t handler);

功能:
注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。

参数:
signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l ("l" 为字母)进行相应查看。

handler: 取值有 3 种情况:
	SIG_IGN:忽略该信号
	SIG_DFL:执行系统默认动作	
	信号处理函数名:自定义信号处理函数,如:fun

回调函数的定义如下:
void fun(int signo)
{
	// signo 为触发的信号,为 signal() 第一个参数的值
}
注意:信号处理函数应该为可重入函数。

返回值:
	成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
	失败:返回 SIG_ERR
代码语言:javascript
复制
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
 
// 信号处理函数
void signal_handler(int signo)
{
	if(signo == SIGINT){
		printf("recv SIGINT\n");
	}else if(signo == SIGQUIT){
		printf("recv SIGQUIT\n");
	}
}
 
int main(int argc, char *argv[])
{
	printf("wait for SIGINT OR SIGQUIT\n");
	
	/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */
	// 信号注册函数
	signal(SIGINT, signal_handler);
	signal(SIGQUIT, signal_handler);
	
	// 等待信号
	pause();
	pause();
	
	return 0;
}
代码语言:javascript
复制
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
 
// 回调函数的声明
typedef void (*sighandler_t)(int);
 
void fun1(int signo)
{
	printf("in fun1\n");
}
 
void fun2(int signo)
{
	printf("in fun2\n");
}
 
int main(int argc, char *argv[])
{
	sighandler_t previous = NULL;
	
	// 第一次返回 NULL
	previous = signal(SIGINT,fun1); 
	if(previous == NULL)
	{
		printf("return fun addr is NULL\n");
	}
	
	// 下一次返回此信号上一次注册的信号处理函数的地址。
	previous = signal(SIGINT, fun2);
	if(previous == fun1)
	{
		printf("return fun addr is fun1\n");
	}
	
	// 还是返回 NULL,因为处理的信号变了
	previous = signal(SIGQUIT,fun1);
	if(previous == NULL)
	{
		printf("return fun addr is NULL\n");
	}
	
	return 0;
}

信号集、阻塞信号集和未决信号集

为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集。 其定义路径为:/usr/include/i386-linux-gnu/bits/sigset.h。

阻塞信号集的作用是影响未决信号集,相当于给他挡了一堵墙。

如果我们把2号信号设置成阻塞(即在阻塞信号集的对应位置设为1),那么来一个2号信号,则未信号集的对应值置为1,什么时候阻塞信号集中的对应位置变成0了,什么时候未决信号集才能去处理之前被阻塞的那个信号。

在这里插入图片描述
在这里插入图片描述

不难理解吧。

代码语言:javascript
复制
#include <signal.h>  
int sigemptyset(sigset_t *set);  
int sigfillset(sigset_t *set);  
int sigismember(const sigset_t *set, int signum);  
int sigaddset(sigset_t *set, int signum);  
int sigdelset(sigset_t *set, int signum);
int sigpending(sigset_t *set);//获取未决信号集
代码语言:javascript
复制
#include <signal.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
	sigset_t set;	// 定义一个信号集变量
	int ret = 0;
 
	sigemptyset(&set); // 清空信号集的内容
	
	// 判断 SIGINT 是否在信号集 set 里
	// 在返回 1, 不在返回 0
	ret = sigismember(&set, SIGINT);
	if(ret == 0){
		printf("SIGINT is not a member of set \nret = %d\n", ret);
	}
		
	sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 set
	sigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set
	
	// 判断 SIGINT 是否在信号集 set 里
	// 在返回 1, 不在返回 0
	ret = sigismember(&set, SIGINT);
	if(ret == 1){
		printf("SIGINT is a member of set \nret = %d\n", ret);
	}
	
	sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除
	
	// 判断 SIGQUIT 是否在信号集 set 里
	// 在返回 1, 不在返回 0
	ret = sigismember(&set, SIGQUIT);
	if(ret == 0){
		printf("SIGQUIT is not a member of set \nret = %d\n", ret);
	}
	
	return 0;
}

每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞。

所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。

我们可以通过 sigprocmask() 修改当前的信号掩码来改变信号的阻塞情况。

代码语言:javascript
复制
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。

参数:
    how: 信号阻塞集合的修改方法,有 3 种情况:
        SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。
        SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。
        SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。

    set: 要操作的信号集地址。
        若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。

    oldset: 保存原先信号阻塞集地址
    
返回值:
    成功:0,
    失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。

注意:不能阻塞 SIGKILL 和 SIGSTOP 等信号,但是当 set 参数包含这些信号时 sigprocmask() 不返回错误,只是忽略它们。另外,阻塞 SIGFPE 这样的信号可能导致不可挽回的结果,因为这些信号是由程序错误产生的,忽略它们只能导致程序无法执行而被终止。

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
 
int main(int argc, char *argv[])
{
	sigset_t set; // 信号集合
	int i = 0;
	
	sigemptyset(&set); // 清空信号集合
	sigaddset(&set, SIGINT); // SIGINT 加入 set 集合
	
	while(1)
	{
		// set 集合加入阻塞集,在没有移除前,SIGINT 会被阻塞
		sigprocmask(SIG_BLOCK, &set, NULL);
		for(i=0; i<5; i++)
		{
			printf("SIGINT signal is blocked\n");
			sleep(1);
		}
		
		// set 集合从阻塞集中移除
		// 假如 SIGINT 信号在被阻塞时发生了
		// 此刻,SIGINT 信号立马生效,中断当前进程
		sigprocmask(SIG_UNBLOCK, &set, NULL);
		for(i=0; i<5; i++)
		{
			printf("SIGINT signal unblocked\n");
			sleep(1);
		}
	}
	
	return 0;
}

signal() 函数主要用于前面 32 种不可靠、非实时信号的处理,并且不支持信号传递信息。

Linux 提供了功能更强大的 sigaction() 函数,此函数可以用来检查和更改信号处理操作,可以支持可靠、实时信号的处理,并且支持信号传递信息。

代码语言:javascript
复制
#include <signal.h>
    
int sigqueue(pid_t pid, int sig, const union sigval value);

功能:
    给指定进程发送信号。

参数:
    pid: 进程号。
    sig: 信号的编号,这里可以填数字编号,也可以填信号的宏定义。
    value: 通过信号传递的参数。

            union sigval  
            {  
                int   sival_int;  
                void *sival_ptr;  
            };  

返回值:
    成功:0
    失败:-1
代码语言:javascript
复制
int sigaction(int signum,const struct sigaction *act, struct sigaction *oldact );

功能:
    检查或修改指定信号的设置(或同时执行这两种操作)。

参数:
    signum:要操作的信号。
    act:   要设置的对信号的新处理方式(设置)。
    oldact:原来对信号的处理方式(设置)。

    如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式(设置)存入 oldact。

		struct sigaction  
    {  
        /*旧的信号处理函数指针*/  
        void (*sa_handler)(int signum) ;  
          
        /*新的信号处理函数指针*/  
        void (*sa_sigaction)(int signum, siginfo_t *info, void *context);  
          
        sigset_t sa_mask;/*信号阻塞集*/  
          
        int sa_flags;/*信号处理的方式*/  
    };  
		
		sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给 sa_sigaction、sa_handler 两者之一赋值,其取值如下:

    SIG_IGN:忽略该信号
    SIG_DFL:执行系统默认动作
    处理函数名:自定义信号处理函数

	sa_mask:信号阻塞集
	sa_flags:用于指定信号处理的行为,它可以是一下值的“按位或”组合:
	    SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)
	    SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
	    SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
	    SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
	    SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
	    SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
	    
返回值:
    成功:0
    失败:-1

信号处理函数:

代码语言:javascript
复制
void (*sa_sigaction)( int signum,  siginfo_t *info,  void *context );
参数说明:

    signum:信号的编号。
    info:记录信号发送进程信息的结构体,进程信息结构体路径:/usr/include/i386-linux-gnu/bits/siginfo.h,其结构体详情请点此链接。
    context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文,其结构体详情点此链接。

下面我们做这么一个例子,一个进程在发送信号,一个进程在接收信号的发送。

代码语言:javascript
复制
    #include <stdio.h>  
    #include <signal.h>  
    #include <sys/types.h>  
    #include <unistd.h>  
      
    /******************************************************* 
    *功能:     发 SIGINT 信号及信号携带的值给指定的进程 
    *参数:        argv[1]:进程号 
                argv[2]:待发送的值(默认为100) 
    *返回值:   0 
    ********************************************************/  
    int main(int argc, char *argv[])  
    {  
        if(argc >= 2)  
        {  
            pid_t pid,pid_self;  
            union sigval tmp;  
      
            pid = atoi(argv[1]); // 进程号  
            if( argc >= 3 )  
            {  
                tmp.sival_int = atoi(argv[2]);  
            }  
            else  
            {  
                tmp.sival_int = 100;  
            }  
              
            // 给进程 pid,发送 SIGINT 信号,并把 tmp 传递过去  
            sigqueue(pid, SIGINT, tmp);  
              
            pid_self = getpid(); // 进程号  
            printf("pid = %d, pid_self = %d\n", pid, pid_self);  
              
        }  
          
        return 0;  
    }  



    #include <signal.h>  
    #include <stdio.h>  
      
    // 信号处理回电函数  
    void signal_handler(int signum, siginfo_t *info, void *ptr)  
    {  
        printf("signum = %d\n", signum); // 信号编号  
        printf("info->si_pid = %d\n", info->si_pid); // 对方的进程号  
        printf("info->si_sigval = %d\n", info->si_value.sival_int); // 对方传递过来的信息  
    }  
      
    int main(int argc, char *argv[])  
    {  
        struct sigaction act, oact;  
          
        act.sa_sigaction = signal_handler; //指定信号处理回调函数  
        sigemptyset(&act.sa_mask); // 阻塞集为空  
        act.sa_flags = SA_SIGINFO; // 指定调用 signal_handler  
          
        // 注册信号 SIGINT  
        sigaction(SIGINT, &act, &oact);  
          
        while(1)  
        {  
            printf("pid is %d\n", getpid()); // 进程号  
              
            pause(); // 捕获信号,此函数会阻塞  
        }  
          
        return 0;  
    }  

abort:直接给自己发送异常信号,直接退出

代码语言:javascript
复制
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
 
int main(){
	sleep(2);
	abort();//kill(getpid(), sig)
	return 0;
}

信号含义表

  1. SIGHUP 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

  1. SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
  2. SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
  3. SIGILL 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
  4. SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用。
  5. SIGABRT 调用abort函数生成的信号。
  6. SIGBUS 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
  7. SIGFPE 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
  8. SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
  9. SIGUSR1 留给用户使用
  10. SIGSEGV 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
  11. SIGUSR2 留给用户使用
  12. SIGPIPE 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
  13. SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
  14. SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
  15. SIGCHLD 子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。

  1. SIGCONT 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
  2. SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
  3. SIGTSTP 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
  4. SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
  5. SIGTTOU 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
  6. SIGURG 有"紧急"数据或out-of-band数据到达socket时产生.
  7. SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
  8. SIGXFSZ 当进程企图扩大文件以至于超过文件大小资源限制。
  9. SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
  10. SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
  11. SIGWINCH 窗口大小改变时发出.
  12. SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作.
  13. SIGPWR Power failure
  14. SIGSYS 非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP 不能恢复至默认动作的信号有:SIGILL,SIGTRAP 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ 默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/10/04 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 什么是信号?
    • 信号状态
      • 信号的默认处理方式
        • 一个完整信号周期
          • 信号的产生方式
            • 发送信号
              • 等待信号
                • 处理信号
                • 信号集、阻塞信号集和未决信号集
                • abort:直接给自己发送异常信号,直接退出
                • 信号含义表
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档