linux c信号(1)-基本概念

2018-06-08 07:28:34

信号是事件发生时对进程的通知机制。有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确时间。

一个(具有合适权限的)进程能够向另一进程发送信号。信号的这一用法可作为一种同步技术,甚至是进程间通信( IPC )的原始形式。进程也可以向自身发送信一号。然而,发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下。
1.硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。硬件异常的例子包括执行一条异常的机器语言指令,诸如,被 0 除,或者引用了无法访问的内存区域。
2.用户键入了能够产生信号的终端特殊字符。其中包括中断字符(通常是 Control-C )、暂停字符(通常是 Control-Z )。
3.发生了软件事件。例如,针对文件描述符的输出变为有效,调整了终端窗口大小,定时器到期,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。

一般进程里为了防止重要的事情处理时被信号打断,一般用signal mask先屏蔽,此时收到的信号都会是pending状态,待后面取消屏蔽时候会触发。

/proc/PID/status 包含了各种位掩码字段,以16进制显示,最低位为信号1,相邻左边为信号2,以此类推。这些字段分别为

 名称 描述
 SigPnd 线程等待信号
 ShdPnd 进程等待信号
 SigBlk 阻塞信号
 SigIgn 忽略信号
 SigCgt 捕获信号
#define	SIGHUP		1	/* Hangup 重新读取配置文件  */
#define	SIGINT		2	/* Interrupt Control-c  */
#define	SIGQUIT		3	/* Control-\ */
#define	SIGILL		4	/* Illegal instruction (ANSI).  */
#define	SIGABRT		SIGIOT	/*  produce a core dump for debugging  */
#define	SIGTRAP		5	/* Trace trap (POSIX).  */
#define	SIGIOT		6	/* IOT trap (4.2 BSD).  */
#define	SIGEMT		7	/* EMT trap (4.2 BSD).  */
#define	SIGFPE		8	/* Floating-point exception (ANSI).  */
#define	SIGKILL		9	/* Kill, unblockable   */
#define	SIGBUS		10	/* Bus error (4.2 BSD).  */
#define	SIGSEGV		11	/* Segmentation violation (ANSI).  */
#define	SIGSYS		12	/* Bad argument to system call (4.2 BSD).  */
#define	SIGPIPE		13	/* Broken pipe (POSIX).  */
#define	SIGALRM		14	/* 由 alarm() or setitimer() 生成 */
#define	SIGTERM		15	/* Termination kill的默认值  */
#define	SIGURG		16	/* Urgent condition on socket (4.2 BSD).  */
#define	SIGSTOP		17	/* Stop, unblockable (POSIX).  */
#define	SIGTSTP		18	/* job-control stop signal  */
#define	SIGCONT		19	/* Continue (POSIX).  */
#define	SIGCHLD		20	/* Child status has changed (POSIX).  */
#define	SIGCLD		SIGCHLD	/* Same as SIGCHLD (System V).  */
#define	SIGTTIN		21	/* Background read from tty (POSIX).  */
#define	SIGTTOU		22	/* Background write to tty (POSIX).  */
#define	SIGIO		23	/* I/O now possible (4.2 BSD).  */
#define	SIGPOLL		SIGIO	/* Same as SIGIO? (SVID).  */
#define	SIGXCPU		24	/* CPU limit exceeded (4.2 BSD).  */
#define	SIGXFSZ		25	/* File size limit exceeded (4.2 BSD).  */
#define	SIGVTALRM	26	/* Virtual alarm clock (4.2 BSD).  */
#define	SIGPROF		27	/* Profiling alarm clock (4.2 BSD).  */
#define	SIGWINCH	28	/* Window size change (4.3 BSD, Sun).  */
#define SIGINFO		29	/* Information request (4.4 BSD).  */
#define	SIGUSR1		30	/* User-defined signal 1 (POSIX).  */
#define	SIGUSR2		31	/* User-defined signal 2 (POSIX).  */
#define SIGLOST		32	/* Resource lost (Sun); server died (GNU).  */

改变信号处置:signal()

signal() 为比较老的处理函数,可移植性不如sigaction(),功能也没它强大。sigaction()是首选的信号处理。不过signal()简单。

#include <signal.h>
void ( *signal(int sig, void (*handler)(int)) ) (int);

//成功返回之前的处理函数指针,失败返回SIG_ERR

//函数原型也可以这样写
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);

如下代码暂时建立信号处置,然后还原,signal() 做不到 获取到当前的信号处置而不改变信号处置,sigaction() 则能做到。

void (*oldHandler)(int);
oldHandler = signal(SIGINT, newHandler);
if (oldHandler == SIG_ERR)
    perror("signal");

//do something

if (signal(SIGINT, oldHandler) == SIG_ERR)
    perror("signal");

handler 也可以指定为如下值,所以调用signal() 也有可能返回如下其中一个值。

SIG_DEL 将信号重置为默认值。
SIG_IGN 忽略信号。

if (signal(SIGINT, SIG_DFL) == SIG_ERR) perror("signal");
if (signal(SIGINT, SIG_IGN) == SIG_ERR) perror("signal");

信号处理简介

调用信号处理器程序,可能会随时打断主程序流程。内核代表进程来调用处理器程序,当处理器返回时,主程序会在处理器打断的位置恢复执行。

#include <signal.h>
#include <sys/types.h>  /* Type definitions used by many programs */
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */
#include <string.h>     /* Commonly used string-handling functions */

static void
sigHandler(int sig)
{
    printf("hello freecls!\n");
}

int
main(int argc, char *argv[])
{
    int j;
    if (signal(SIGINT, sigHandler) == SIG_ERR)
        return 1;

    for (j = 0; ; j++) {
        printf("%d\n", j);
        sleep(3);                       /* Loop slowly... */
    }
}

该程序执行时,当你按下ctrl+c时,不会终止程序(因为默认行为被改变了)而会输出 helo freecls 然后继续执行。最后按下 ctrl+\终止。

发送信号:kill()

与shell 的kill 命令类似,一个进程可以向另一个进程发送信号(必须要有权限)。

#include <signal.h>

int kill(pid_t pid, int sig);

//成功返回0, 失败-1

//pid > 0发送为指定进程
//pid = 0发送给当前进程组里所有进程包括自己
//pid < -1 发送给当前进程组里的进程组id = pid\
//的绝对值 = killpg(pid_t pgrp, int sig);
//pid = -1发送给所有进程(必须有权限)除了init和自己
//sig = 0为空信号用来检测进程是否存在,不可靠因为进程id可以重复使用,或是僵尸进程

特权级进程可以向任何进程发送信号,非特权级必须满足如下图条件。

向自己发送信号:raise()

#include <signal.h>

int raise(int sig);

//成功返回0,失败返回非0

//单线程程序相当于
kill(getpid(), sig);

//多线程相当于
pthread_kill(pthread_self(), sig);

向进程组发送信号

#include <signal.h>

int killpg(pid_t pgrp, int sig);

//成功返回0,失败-1

//相当于
kill(-pgrp, sig);

如果pgrp=0,则向进程组所有进程发送信号。

信号描述

每个信号都有一段与之相对应的描述,可以用sys_siglist[sig] 来获取,但是推荐用strsignal() 函数,因为它支持locale。

#define _BSD_SOURCE
#include <signal.h>

extern const char *const sys_siglist[];

#define _GNU_SOURCE
#include <string.h>

char *strsignal(int sig);

直接打印到错误输出

#include <signal.h>

void psignal(int sig, const char *msg);
#include <signal.h>
#include <stdio.h>
#include <string.h>
extern const char *const sys_siglist[];
char *strsignal(int sig);

int main(){
    char *tmp = strsignal(SIGINT);
    printf("%s\n", tmp);
    
    psignal(SIGINT,"sigerr");
}
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 
Interrupt
sigerr: Interrupt

信号集

许多信号相关的系统调用都需要能表示一组不同的信号。多个信号可使用一个称之为信号集的数据结构来表示。数据类型为 sigset_t。

sigemptyset() 初始化一个未包含任何成员的信号集。sigfillset() 初始化一个信号集,包含所有信号(包括实时信号)。

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

//成功返回0,失败-1

必须使用上述2个函数初始化信号集,因为C语言不会对自动变量进行初始化。

添加或移除单个信号

#include <signal.h>

int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);

//成功返回0,失败-1

测试信号 sig 是否为 set的成员

#include <signal.h>

int sigismember(const sigset_t *set, int sig);

//1表示是,0不是

另外3个非标准函数

#define _GNU_SOURCE
#include <signal.h>

//left与right交集
int sigandset(sigset_t *set, sigset_t *left, sigset_t *right);

//left与right并集
int sigorset(sigset_t *dest, sigset_t *left, sigset_t *right);

//成功返回0,失败-1

//判断是否为空,是返回1,不是0
int sigisemptyset(const sigset_t *set);

信号掩码(阻塞信号传递)

也可叫信号遮罩,信号屏蔽。内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程传递。在多线程环境中,每个线程都可以使用pthread_sigmask()函数来独立检查和修改其信号。

#include <signal.h>

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

//Returns 0 on success, or –1 on error

sigprocmask() 既可以修改信号掩码,又可获取现有的掩码,或两者兼具。

how参数指定了该函数信号掩码带来的变化:

SIG_BLOCK 将set 指向信号集并集到当前信号掩码中。

SIG_UNBLOCK 将set指向的信号集中的信号从信号掩码中移除,就算信号掩码中没有要解除的信号也不报错。

SIG_SETMASK 将set指向的信号集设置为信号掩码。

如果 oldset 参数不为NULL,则其指向sigset_t 结构缓冲区,用于返回之前的信号掩码。

如果只是想获取信号掩码, set 参数设为NULL即可,这时将忽略how 参数。

如果解除了对某个等待信号的阻塞,会立刻将该信号传递给进程。

下面代码时暂时阻止 SIGINT 信号的传递。

sigset_t blockSet, prevMask;

sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT);

if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1)
    perror("sigprocmask1");

if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1)
    perror("sigprocmask2");

SIGKILLSIGSTOP 信号是不允许阻塞的,也就是下面的代码会阻塞除这2个信号以外的任何信号。

sigfillset(&blockSet);
if (sigprocmask(SIG_BLOCK, &blockSet, NULL) == -1)
    perror("sigprocmask");

处于等待中的信号

如果某进程接收了一个正在阻塞的信号,那么会将该信号添加到进程的等待信号集中。之后如果解除了对该信号的阻塞,就会把该信号传递给此进程(就算在阻塞期间发生了N次,解除时只会传递1次,而实时信号可以排队)。

sigpending() 系统调用返回进程处于等待状态的信号集,并存入set 指向的sigset_t 结构中。

#include <signal.h>

int sigpending(sigset_t *set);

//Returns 0 on success, or –1 on error

改变信号处置:sigaction()

除signal() 之外,sigaction() 系统调用是设置信号处置的另一选择。用法复杂一点,但是功能强大,且可移植性强。

#include <signal.h>

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;

    int sa_flags; /* Flags controlling handler invocation */
    void (*sa_restorer)(void); /* Not for application use */
};

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

//Returns 0 on success, or –1 on error

sig为想要获取或改变的信号编号,除去 SIGKILL SIGSTOP。act是指向信号新处置的数据结构,oldact用来返回之前的信号处置结构,都可以设置为 NULL。

sigaction 结构实际更为复杂,下面文章再讲。sa_handler 对应于 signal()的handler。其所对应的值为信号处理器函数或是SIG_IGN、SIG_DFL。仅当sa_handler 为函数地址时,才会对sa_mask 和 sa_flags 加以处理。

sa_mask 定义了一组信号,在调用sa_handler 所定义的处理程序时阻塞,也就是在调用处理器程序之前,将这些信号添加到信号掩码中。处理器函数返回时自动删除。利用这一特性可以指定哪些信号不允许打断处理器的执行。此外,引发此处理器程序的信号也将自动添加到信号掩码中。这就保证了同一个信号抵达多次不会递归中断自己。

sa_flags字段是一个位掩码,可以是如下的选项相或(|):

SA_NOCLDSTOP 若 sig 为SIGCHLD 信号,则当因接受一信号而停止或恢复某一子进程时,将不产生此信号。

SA_NOCLDWAIT 若 sig 为SIGCHLD 信号,则当子进程终止时不会将其转化为僵尸进程。

SA_NODEFER 捕获信号时,不会在执行处理器程序时自动将该信号添加到进程掩码中。别名 SA_NOMASK。

SA_ONSTACK 针对此信号调用处理器函数时,使用了由 sigaltstack() 安装的备选栈。

SA_RESETHAND 当捕获该信号时,会在调用处理函数之前将信号处置重置为默认值(SIG_DFL)。

SA_RESTART 自动重启由信号处理器程序中断的系统调用。

SA_SIGINFO 调用信号处理器程序时携带额外的参数。

等待信号:pause()

#include <unistd.h>

int pause(void);

//Always returns –1 with errno set to EINTR

暂停进程的执行,直至信号处理器函数中断该调用为止(或者直至一个未处理信号终止进程为止)。


总结

下一篇继续,如果有疑问可以给我留言。


备注
1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/4c

©著作权归作者所有
收藏
推荐阅读
简介
天降大任于斯人也,必先苦其心志。