赞
踩
信号是由用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常,Linux中有很多种不同的信号。可以在终端输入kill -l 来查看Linux支持的信号,如下图:
Linux信号可由如下条件产生:
1、对于前台进程,用户可以通过输入特殊的终端字符来发送,比如输入Ctrl+C通常会给正在运行的进程发送一个中断信号。
2、系统异常。比如浮点异常和非法内存段访问。
3、系统状态变化。比如alarm定时器到期时将引起SIGALRM信号。
4、在终端运行kill命令或在程序中调用kill函数,例如:如果要杀死一个进程,我们可以使用 kill -9 pid 来杀死进程,-9表示发送9号信号,也就是SIGKILL信号,pid为发送信号的目标进程的进程ID;9号信号无法被忽略以及改变默认处理方式,因此发送9号信号一定能杀死进程。
Linux中,给进程发送信号的系统调用为kill,定义如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); // 给进程ID为pid的进程发送sig信号
pid参数 | 含义 |
---|---|
pid > 0 | 信号发送给进程ID为pid的进程。 |
pid = 0 | 信号发送给本进程组内的其它进程。 |
pid = -1 | 信号发送给除init进程外的所有进程,需要有权限 |
pid < -1 | 信号发送给ID为-pid的进程组中的所有成员 |
kill函数在成功时返回0, 失败时返回-1,并设置errno。
每个信号都有默认的处理方式,有的信号的默认处理方式是终止进程,有的是忽略信号以及结束进程并生成核心转储文件、暂停进程以及继续进程等。我们也可以在程序中修改信号的处理方式,注意:无法修改9号信号SIGKILL的默认处理方式,也无法忽略该信号。
信号处理函数的原型为:
#include <signal.h>
typedef void (*sighandler_t)(int);
除了用户自定义信号处理函数外,bits/signum.h头文件还定义了信号的两种其它处理方式:
#include <bits/signum.h>
#define SIG_DFL ((sighandler_t) 0) // 使用信号的默认处理方式
#define SIG_IGN ((sighandler_t) 1) // 忽略目标信号
要为一个信号设置处理函数,可以使用signal系统调用:
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
signum为要捕获的信号类型,handler用于指定新的信号处理函数,也就是程序在收到signum类型信号后执行的回调函数。返回值为旧的信号处理函数。
例如下面代码:
代码中修改了2号信号SIGINT的处理函数,SIGINT信号可以由终端中按Ctrl+C来产生,因此当我们运行该程序后按Ctrl+C就会输出hello,world,为了结束该进程我们可以按Ctrl+\,这个按键组合会产生SIGQUIT信号,该信号的默认处理函数是结束进程并产生转储文件。如下图所示:
#include <signal.h> #include <iostream> #include <unistd.h> // 信号处理回调函数 void handleSignal(int signum) { std::cout << "hello, world!" << std::endl; } int main() { signal(SIGINT, handleSignal); //修改SIGINT信号的处理方式 while(1) { sleep(1); } return 0; }
在下面的代码中修改9号信号SIGKILL的信号处理函数进行一个测试:
#include <signal.h> #include <iostream> #include <string.h> #include <errno.h> #include <unistd.h> typedef void (*sighandler_t)(int); void sig_handler(int signum) { std::cout << "hello, world!" << std::endl; } int main() { sighandler_t ret = signal(SIGKILL, sig_handler); if( ret == SIG_ERR) { std::cout << "ignore SIGKILL failed, reason: " << strerror(errno) << std::endl; } while(1) { sleep(1); } return 0; }
然后编译运行该程序,如下图:
发现signal系统调用失败了,打开另一个终端查看该进程ID,并发送9号信号给此进程,发现进程还是被杀死了。说明9号信号SIGKILL的默认处理动作是无法被修改的。而且该信号也是不能被忽略的。
sigaction系统调用:
#include <signal.h>
// act为新的处理方式,odlact为旧的处理方式
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 第二种形式的信号处理函数
// 屏蔽信号集,调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sigset_t sa_mask;
int sa_flags; // 通常设置为0,表使用默认属性
void (*sa_restorer)(void); // 过时的元素,弃用
};
使用sigaction函数时可以指定在信号处理函数被调用的过程中要屏蔽的信号。
进程或用户A给一个进程B发送信号,B在收到信号之前执行自己的代码,当B进程收到信号后,不管程序执行到什么位置,都要暂停运行,去处理信号,也就是调用信号处理函数,处理完再继续执行。与硬件中断类似——异步模式。但信号是软件层面实现的中断,早期常被成为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
内核实现信号捕捉过程:
Linux使用数据结构sigset_t来表示一组信号,定义如下:
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
typedef struct
{
unsigned long int _val[_SIGSET_NWORDS];
} sigset_t;
// sigset_t 实际上是一个长整型数组,数组中每个元素的每个位表示一个信号,Linux提供了如下一组函数来设置、修改、删除和查询信号集:
int sigemptyset(sigset_t *set); //将信号集清0 成功:0;失败:-1
int sigfillset(sigset_t *set); //将信号集置1 成功:0;失败:-1
int sigaddset(sigset_t *set, int signum); //将信号加入信号集 成功:0;失败:-1
int sigdelset(sigset_t *set, int signum); //将信号清出信号集 成功:0;失败:-1
int sigismember(const sigset_t *set, int signum); //判断某个信号是否在信号集中 返回值:在集合:1;不在:0;出错:-1
我们可以利用sigprocmask来设置进程的信号掩码。该函数可以用来设置进程要屏蔽的信号或者解除屏蔽的信号,其本质,读取或修改进程的信号掩码(也叫信号屏蔽集)(PCB中)。进程的PCB中保存了该进程的信号屏蔽集,该屏蔽集用来指示哪些信号在产生时会被屏蔽。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/* how参数:
SIG_BLOCK: 新的进程信号掩码是其当前值和set指定信号集的并集,相当于 mask = mask|set
SIG_UNBLOCK: 新的信号掩码是其当前值和~set信号集的交集,因此set指定的信号集将不被屏蔽,相当于 mask = mask & ~set
SIG_SETMASK: 直接将进程信号掩码设置为set
sigprocmask成功时返回0,失败时返回-1并设置errno。
设置信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。下面的函数可以获得进程当前被挂起的信号集。
#include <signal.h>
int sigpending(sigset_t *set);
在信号屏蔽期间如果该信号多次产生,在屏蔽结束后进程也只能接收到一次该信号。
例如下面代码:在main函数中分别注册了SIGINT和SIGQUIT信号的处理函数,并屏蔽了SIGINT信号,SIGINT信号可由终端按Ctrl+C产生,因此当我们按这个按键组合时,进程无法收到信号。当我们使用Ctrl+\来产生SIGQUIT信号时,由于该信号没有被屏蔽因此其处理函数会被调用,解除对SIGINT信号的屏蔽,之后进程便可以收到SIGINT信号,并输出hello,world。但是无论按了多少次Ctrl+C,hello,world只会输出一次。如下图:
#include <signal.h> #include <unistd.h> #include <iostream> // SIGINT 信号处理函数 void handle_sigint(int signum) { std::cout << "hello,world!" << std::endl; } // SIGQUIT 信号处理函数 void handle_sigquit(int signum) { // 解除SIGINT信号的屏蔽 sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_UNBLOCK, &set, nullptr); } int main() { // 注册SIGINT和SIGQUIT的信号处理函数 signal(SIGINT, handle_sigint); signal(SIGQUIT, handle_sigquit); // 屏蔽SIGINT信号 sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_BLOCK, &set, nullptr); while(1) { sleep(1); } return 0; }
但是上面的代码会出现一个让人奇怪的情况,当按下Ctrl+\解除了信号屏蔽之后,进程会立即收到SIGINT信号,但是再按Ctrl+C,将会发现SIGINT信号又被屏蔽了。但是如果在main函数中解除信号屏蔽,那么之后再发送SIGINT信号,进程都将立刻收到,如下图:
代码如下:
#include <signal.h> #include <unistd.h> #include <iostream> bool flag = false; void handle_sigint(int signum) { std::cout << "hello,world!" << std::endl; } void handle_sigquit(int signum) { flag = true; } int main() { signal(SIGINT, handle_sigint); signal(SIGQUIT, handle_sigquit); sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_BLOCK, &set, nullptr); while(1) { sleep(1); if(flag) { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_UNBLOCK, &set, nullptr); flag = false; } } return 0; }
关于为什么会出现这样的情况,可能是进入信号处理函数后,修改的信号掩码是临时的,在信号处理函数调用完毕后,又恢复了原来的mask。
SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
SIGPWR:关机。默认动作为终止进程。
SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
SIGRTMIN ~ 64 SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
系统调用可以分为两类:
慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait…
其他系统调用:getpid、getppid、fork…
如果程序在执行处于阻塞状态的系统调用时收到信号,则默认情况下系统调用将会被中断,并且errno被设置为EINTR。我们可以使用sigaction函数为信号设置SA_RESTART标志自动启动被该信号中断的系统调用。
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽太久(为了避免一些竞态条件,在调用信号处理函数期间,该信号不会被再次触发)。一种解决方案是:将信号的主要处理逻辑放在程序的主循环中,当信号处理函数触发时,它只是简单地通知主循环接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行对应的处理逻辑。信号处理函数通常使用管道来将信号传递给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据呢?我们只需使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件即可。如此一来,信号事件就能和其它I/O事件一样被处理,即统一事件源。
——《Linux高性能服务器编程》
下面实现的回射服务器可以将I/O事件以及信号事件统一在主循环中进行处理:
#include <iostream> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <string.h> #include <errno.h> #include <netinet/in.h> #include <netinet/ip.h> #include <sys/epoll.h> #include <fcntl.h> #include <arpa/inet.h> #include <signal.h> using std::cout; using std::endl; #define PORT 6666 #define MAX_EVENTS 1024 #define MAX_BUF_SIZE 1024 struct Event; using readHandle = void(*)(Event *); using writeHandle = void(*)(Event *); // 自定义结构体,用来保存一个连接的相关数据 struct Event { int fd; char ip[64]; uint16_t port; epoll_event event; char buf[MAX_BUF_SIZE]; int buf_size; readHandle read_cb; writeHandle write_cb; }; int epfd; static int pipefd[2]; void err_exit(const char *reason) { cout << reason << ":" << strerror(errno) << endl; exit(1); } // 设置非阻塞 int setNonblcoking(int fd) { int old_option = fcntl(fd, F_GETFL); int new_option = old_option | O_NONBLOCK; fcntl(fd, F_SETFL, new_option); return old_option; } // 设置端口复用 void setReusedAddr(int fd) { int reuse = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); } // 信号处理函数 void sig_handler(int signum) { // 保留原来的errno,在函数最后恢复,以保证函数的可重入性 int save_errno = errno; int msg = signum; send(pipefd[1], (char *)&msg, 1, 0); // 将信号写入管道 errno = save_errno; } // 设置信号的处理函数 void addsig(int sig) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = sig_handler; sa.sa_flags |= SA_RESTART; sigfillset(&sa.sa_mask); sigaction(sig, &sa, nullptr); } // 初始化server socket int socket_init(unsigned short port, bool reuseAddr) { int fd = socket(AF_INET, SOCK_STREAM, 0); if(fd < 0) { err_exit("socket error"); } if(reuseAddr) { setReusedAddr(fd); } struct sockaddr_in addr; bzero(&addr, 0); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr)); if(ret < 0) { err_exit("bind error"); } setNonblcoking(fd); ret = listen(fd, 128); if(ret < 0) { err_exit("listen error"); } return fd; } void readData(Event *ev) { ev->buf_size = read(ev->fd, ev->buf, MAX_BUF_SIZE - 1); ev->event.events = EPOLLOUT; epoll_ctl(epfd, EPOLL_CTL_MOD, ev->fd, &ev->event); } void writeData(Event *ev) { write(ev->fd, ev->buf, ev->buf_size); ev->event.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_MOD, ev->fd, &ev->event); } // 接收连接回调函数 void acceptConn(Event *ev) { Event *cli = new Event; struct sockaddr_in cli_addr; socklen_t sock_len = sizeof(cli_addr); int cfd = accept(ev->fd, (struct sockaddr *)&cli_addr, &sock_len); if(cfd < 0) { cout << "accept error, reason:" << strerror(errno) << endl; return; } setNonblcoking(cfd); cli->fd = cfd; cli->port = ntohs(cli_addr.sin_port); inet_ntop(AF_INET, &cli_addr.sin_addr, cli->ip, sock_len); cli->read_cb = readData; cli->write_cb = writeData; cli->event.events = EPOLLIN; cli->event.data.ptr = (void *) cli; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &cli->event); cout << "New Connection, ip:[" << cli->ip << ":" << cli->port << "]" << endl; } int main(int argc, char *argv[]) { int fd = socket_init(PORT, true); Event server; server.fd = fd; epfd = epoll_create(MAX_EVENTS); if(epfd < 0) { err_exit("epoll create error"); } server.event.events = EPOLLIN; server.event.data.ptr = (void *)&server; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &server.event); // 使用socketpair创建管道,注册pipefd[0]上的可读事件 int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd); if(ret == -1) { err_exit("socketpair error"); } setNonblcoking(pipefd[1]); Event pipeEv; pipeEv.fd = pipefd[0]; pipeEv.event.events = EPOLLIN; pipeEv.event.data.ptr = (void *)&pipeEv; epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &pipeEv.event); // 设置一些信号的处理函数 addsig(SIGINT); addsig(SIGCHLD); addsig(SIGTERM); addsig(SIGQUIT); bool stop_server =false; struct epoll_event events[MAX_EVENTS]; int nready = 0; while(!stop_server) { // 将定时容器中定时时间最短的时长作为epoll_wait的最大等待时间 nready = epoll_wait(epfd, events, MAX_EVENTS, 1000); if(nready < 0) { cout << "epoll wait error, reason:" << strerror(errno) << endl; } else if(nready > 0) { for(int i = 0; i < nready; i++) { Event *ev = (Event *) events[i].data.ptr; // 接受新的连接 if(ev->fd == fd ) { acceptConn(ev); } else if(ev->fd == pipefd[0]) //处理信号 { int sig; ret = recv(ev->fd, ev->buf, MAX_BUF_SIZE, 0); if(ret <= 0) { continue; } else { // 每个信号占一个字节,所以按字节来逐个接收信号 for(int i = 0; i < ret; i++) { switch (ev->buf[i]) { case SIGCHLD: cout << "SIGCHLD\n"; break; case SIGQUIT: stop_server = true; break; case SIGTERM: cout << "SIGTERM\n"; case SIGINT: cout << "别按了,休想终止我!\n"; } } } } else if(ev->event.events & EPOLLIN) { ev->read_cb(ev); } else if(ev->event.events & EPOLLOUT) { ev->write_cb(ev); } } } } close(fd); close(pipefd[0]); close(pipefd[1]); return 0; }
当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们常利用SIGHUP信号来强制服务器重新读取配置文件。
默认情况下,往一个读端关闭的管道或socket连接中写入数据将引发SIGPIPE信号,程序收到SIGPIPE信号的默认处理方式是结束进程,而我们绝不希望因为错误的写操作而导致进程退出,因此我们需要在代码中捕获并处理该信号,或者至少忽略它。
我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。在这种情况下我们应该使用send函数反馈的errno值来判断管道或者socket连接的读端是否已经关闭。
由alarm和setitimer函数设置的定时器一旦超时,将触发SIGALRM信号,因此我们可以利用该信号的信号处理函数来处理定时任务。SIGALRM的默认处理是终止进程,而且每个进程都有且只有唯一个定时器。
相关函数如下:
#include <unistd.h> unsigned int alarm(unsigned int seconds); // 返回0或剩余的秒数,无失败 #include <sys/time.h> // us级别的定时,可以实现周期定时 int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); struct itimerval { struct timeval it_interval; /* 用来设定两次定时任务之间间隔的时间。 */ struct timeval it_value; /* 定时的时长 */ }; struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。