linux系统编程之信号(一):信号基本概述

一、为了理解信号,先从我们最熟悉的场景说起:

1. 用户输入命令,在Shell下启动一个前台进程。

2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。

3. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。

4. 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。

5. 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。

用kill -l命令可以察看系统定义的信号列表: 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2。编号34以上的是实时信号,这些信号各自在什么条件下产生,默认的处理动作是什么(Term表示终止当前进程,Core表示终止当前进程并且Core Dump,Ign表示忽略该信号,Stop表示停止当前进程,Cont表示继续执行先前停止的进程),在signal(7)中都有详细说明。

0~31 不可靠信号,多个信号不会排队只保留一个,即信号可能丢失。

34~64 可靠(实时信号),支持排队信号不会丢失,可使用sigqueue发送信号,不像0~31有缺省的定义。

二、产生信号的条件主要有:

1、用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号。

2、硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。

3、再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

4、一个进程调用kill(2)函数可以发送信号给另一个进程。

5、可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。

6、raise:给自己发送信号。raise(sig)等价于kill(getpid(), sig); 7、killpg:给进程组发送信号。killpg(pgrp, sig)等价于kill(-pgrp, sig); 8、sigqueue:给进程发送信号,支持排队,可以附带信息。

9、当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。

三、用户程序可以调用signal(2) / sigaction(2)函数告诉内核如何处理某种信号(若未注册则按缺省处理),可选的处理动作有三种: 1. 忽略此信号。有两个信号不能被忽略:SIGKILL和SIGSTOP。 2. 执行该信号的默认处理动作。 3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

四、信号与中断的区别

信号与中断的相似点: (1)采用了相同的异步通信方式; (2)当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序; (3)都在处理完毕后返回到原来的断点; (4)对信号或中断都可进行屏蔽。 信号与中断的区别: (1)中断有优先级,而信号没有优先级,所有的信号都是平等的; (2)信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行; (3)中断响应是及时的,而信号响应通常都有较大的时间延迟。

五、signal(2) 信号注册函数

typedef void (*__sighandler_t) (int); #define SIG_ERR ((__sighandler_t) -1) #define SIG_DFL ((__sighandler_t) 0) #define SIG_IGN ((__sighandler_t) 1)

函数原型: __sighandler_t signal(int signum, __sighandler_t handler); 参数 signal是一个带signum和handler两个参数的函数,准备捕捉或屏蔽的信号由参数signum给出,接收到指定信号时将要调用的函数由handler给出,handler这个函数必须有一个int类型的参数(即接收到的信号代码),它本身的类型是void handler也可以是两个特殊值:SIG_IGN 忽略该信号;SIG_DFL    恢复默认行为

RETURN VALUE        signal() returns the previous value of the signal handler, or SIG_ERR on error.

示例小程序:

/*************************************************************************
    > File Name: process_.c
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Sat 23 Feb 2013 02:34:02 PM CST
 ************************************************************************/
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);

int main(int argc, char *argv[])
{
    __sighandler_t oldhandler;
    oldhandler = signal(SIGINT, handler); // 返回值是先前的信号处理程序函数指针
    if (oldhandler == SIG_ERR)
        ERR_EXIT("signal error");

    while (getchar() != '\n') ;

    /* signal(SIGINT, SIGDFL) */
    if (signal(SIGINT, oldhandler) == SIG_ERR)
        ERR_EXIT("signal error");
    for (; ;) ;

    return 0;
}

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

测试输出如下:

simba@ubuntu:~/Documents/code/linux_programming/APUE/signal$ ./signal  ^Crecv a sig=2 ^Crecv a sig=2 ^Crecv a sig=2 ^C simba@ubuntu:~/Documents/code/linux_programming/APUE/signal$ 

程序执行开始注册了SIGINT信号的处理函数,故我们按下ctrl+c 并不会像往常一样终止程序,只是打印了recv a  sig = 2。接着按下回车,重新注册了SIGINT的默认处理,此时再ctrl+c 程序就被终止了。

将程序中的 32 ~37 行 换成如下的表述:

for (; ;)
{
    pause(); //使进程挂起直到一个信号被捕获(信号处理函数完成后返回)
    //且调用schedule()使系统调度其他程序运行,
    //这样比完全的死循环的好处是让出cpu
    printf("pause return\n");
}

调用pause函数:将进程置为可中断睡眠状态。然后它调用schedule(),使linux进程调度器找到另一个进程来运行。pause使调用者进程挂起,直到一个信号被捕获处理后函数才返回。调用pause 的好处是在等待信号的时候让出cpu,让系统调度其他进程运行,而不是完全的死循环,当然这样ctrl+c 就是始终终止不了程序,我们可以使用 ctrl+\ 产生SIGQUIT信号终止程序。

事实上根据man手册,signal 函数可移植性并不是很好,最好只是用在SIG_DFL, SIG_IGN 上,注册信号处理函数用sigaction 比较好。

参考:《APUE》、《linux c 编程一站式学习》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

52. Socket Server 自定义协议的简单实现 | 厚土Go学习笔记

在 Server 和 Client 通讯中,由于网络等原因很有可能会发生数据丢包的现象。如果数据确实,服务端接收的信息不完整,就会造成混乱。 我们就需要在 Se...

3869
来自专栏H2Cloud

FFLIb Demo && CQRS

使用FFLIB 构建了一个demo,该demo模拟了一个常见的游戏后台架构,该demo主要有一下亮点: FFLIB 实现进程间通信非常方便 基于CQRS 思想构...

2588
来自专栏杨龙飞前端

http协议缓存小结

缓存可以使用expire方式,设置到期时间,缓存的时间等于expire设置的时间减去当前的时间

1324
来自专栏IT笔记

使用elasticsearch遇到的一些问题以及解决方法

1.由gc引起节点脱离集群 因为gc时会使jvm停止工作,如果某个节点gc时间过长,master ping3次(zen discovery默认ping失败重试3...

3024
来自专栏hbbliyong

由一道面试题来了解进程间的通信

周末面试碰到一个面试题,题目是: 在MMO游戏中,服务器采用Linux操作系统,网络通信与游戏逻辑处理进程一般是分离的。 例如:GameSvr进程处理游戏逻辑,...

5867
来自专栏分布式系统进阶

Kafka源码分析-启动流程

使用getPropsFromArgs方法来获取各配置项, 然后将启动和停止动作全部代理给KafkaServerStartable类;

680
来自专栏小筱月

Java web 前端面试知识点总结

耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口...

962
来自专栏智能大石头

NewLife.Net——管道处理器解决粘包

1263
来自专栏码匠的流水账

nginx lua重置请求参数及常量备忘

731
来自专栏还债之路

redis缓存服务器

#你当前没有指定配置文件,以默认的配置文件启动,如果你想指定配置文件你可以redis-server 文件所在位置

732

扫码关注云+社区