信号
信号是由用户、系统、进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常重点内容**,Linux信号可由以下条件产生:
- 对于前台进程,用户可以通过输入特殊终端字符来给它发送信号,比如输入Ctrl + C通常会给进程发送一个中断信号;
- 系统异常,比如浮点异常和非法内存段访问;
- 系统状态变化,比如alarm定时器到期引起SIGALRM信号;
- 运行kill命令或调用kill函数;
信号种类
信号发送
一个进程给其他进程发送信号的API是kill进程:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
/*
pid > 0 信号发送给PID为pid的进程
pid = 0 信号发送给本进程组内的其他进程
pid = -1 信号发送给出init进程外的所有进程,但发送者需要拥有对目标进程发送信号的权限
pid < -1 信号发送给组ID为-pid的进程组中的所有成员
*/
信号处理方式
每种信号类型都有对应的信号处理程序(也叫信号的操作),就好像每个中断都有一个中断服务例程一样。大多数信号的默认操作是结束接收信号的进程;然而,一个进程通常可以请求系统采取某些代替的操作,各种代替操作是:
- 忽略信号。随着这一选项的设置,进程将忽略信号的出现。有两个信号 不可以被忽略:SIGKILL,它将结束进程;SIGSTOP,它是作业控制机制的一部分,将挂起作业的执行。
- 恢复信号的默认操作。
- 执行一个预先安排的信号处理函数。进程可以登记特殊的信号处理函数。当进程收到信号时,信号处理函数将像中断服务例程一样被调用,当从该信号处理函数返回时,控制被返回给主程序,并且继续正常执行。
但是,信号和中断有所不同,中断的响应和处理都发生在内核空间,而信号的响应发生在内核空间,信号处理程序的执行发生在用户空间。
那么,什么时候检测和响应信号呢?通常发生在两种情况下:
- 当前进程由于系统调用、中断或异常而进入内核空间后,从内核空间返回到用户空间前夕;
- 当前进程在内核中进入睡眠以后刚被唤醒的时候,由于检测到信号的存在而提前返回到用户空间;
目标进程在收到信号时,需要定义一个接收函数来处理,函数原型:
#include<signal.h>
typedef void (*__sighandler_t)(int); //整形参数指示信号类型,信号处理函数应该是可重入的,否则很容易引发一些竞态条件;
信号的默认处理方式有如下几种:
- 结束进程(Term);
- 忽略信号(Ign);
- 结束进程并生成核心转储文件(Core);
- 暂停进程(Stop);
- 继续进程(Cont);
信号函数
signal系统调用
#include<signal.h>
_sighandler_t signal(int sig, //指定信号类型
_sighandler_t _handler /信号处理函数的函数指针
);
sigaction系统调用
sa_mask详解
#include<signal.h>
int sigaction(int sig, //信号类型
const struct sigaction* act, //指定新的信号处理方式
struct sigaction* oact ); //返回先前的处理方式
struct sigaction{
_sighandler_t sa_handler; //信号处理函数
_sigset_t sa_mask; //设置进程的信号掩码
int sa_flags; //用于设置程序收到信号时的行为
};
/* 信号集函数 */
#include<signal.h>
int sigempty(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t* set, int _signo);
int sigdelset(sigset_t* set, int _signo);
int sigismember(_const sigset_t* set, int _signo);
//设置或查看进程的信号掩码
#include<signal.h>
int sigprocmask(int _how, //SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK
_const sigset_t* set, //传入的set
sigset_t* _oset); //传出的set
要始终清楚的知道进程在每个运行时刻的信号掩码,以及如何适当的处理捕获到的信号。在多进程、多线程环境中,我们要以进程、线程为单位来处理信号和信号掩码。我们不能设想新创建的进程、线程具有和父进程、主线程完全相同的信号特征,比如fork调用的子进程将继承父进程的信号掩码,但具有一个空的挂起信号集;
中断系统调用
如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR,我们可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用;
对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(如connect,epoll_wait);
信号的本质
信号是在软件层次上对中断机制的模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求是一样的。信号是异步的,一个进程不必通过任何操作等待信号的到达,事实上,进程也不知道信号什么时候到达。
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
信号处理机制的原理(内核角度)
内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态才处理。进程只有在处理完信号才返回用户态,进程在用户态下不会有未处理完的信号;
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方,接着原来的地方继续运行。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
统一事件源
信号是一种异步事件,信号处理函数和程序的主循环是两条不同的执行路线,很明显,信号处理函数需要尽可能快的执行完毕,以确保该信号不会被屏蔽太久(为了避免一些竞态条件,信号在处理期间,系统不会再次触发它),一种典型的解决方法是:把信号的主要处理逻辑放在程序的主循环中,当信号处理函数被触发时,它只是简单的通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。
信号处理函数通常使用管道将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢?这很简单,我们只需要使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件,如此一来,信号事件就可以和其他I/O事件一样被处理,即统一事件源。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
static int pipefd[2];
int setnonblocking( 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 addfd( int epollfd, int fd )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
setnonblocking( fd );
}
void sig_handler( int sig )
{
int save_errno = errno;
int msg = sig;
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 );
assert( sigaction( sig, &sa, NULL ) != -1 );
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
//int nReuseAddr = 1;
//setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &nReuseAddr, sizeof( nReuseAddr ) );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
if( ret == -1 )
{
printf( "errno is %d\n", errno );
return 1;
}
//assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
epoll_event events[ MAX_EVENT_NUMBER ];
int epollfd = epoll_create( 5 );
assert( epollfd != -1 );
addfd( epollfd, listenfd );
ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd );
assert( ret != -1 );
setnonblocking( pipefd[1] );
addfd( epollfd, pipefd[0] );
// add all the interesting signals here
addsig( SIGHUP );
addsig( SIGCHLD );
addsig( SIGTERM );
addsig( SIGINT );
bool stop_server = false;
while( !stop_server )
{
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
if ( ( number < 0 ) && ( errno != EINTR ) )
{
printf( "epoll failure\n" );
break;
}
for ( int i = 0; i < number; i++ )
{
int sockfd = events[i].data.fd;
if( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd );
}
else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )
{
int sig;
char signals[1024];
ret = recv( pipefd[0], signals, sizeof( signals ), 0 );
if( ret == -1 )
{
continue;
}
else if( ret == 0 )
{
continue;
}
else
{
for( int i = 0; i < ret; ++i )
{
//printf( "I caugh the signal %d\n", signals[i] );
switch( signals[i] )
{
case SIGCHLD:
case SIGHUP:
{
continue;
}
case SIGTERM:
case SIGINT:
{
stop_server = true;
}
}
}
}
}
else
{
}
}
}
printf( "close fds\n" );
close( listenfd );
close( pipefd[1] );
close( pipefd[0] );
return 0;
}
网络编程相关信号
SIGHUP
当挂起进程的控制终端时,SIGHUP信号将被触发。 对于没有控制终端的网络后台程序,他们通常利用SIGHUP信号来强制服务器重读配置文件。
SIGPIPE
默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为在程序接收到SIGPIPE信号的默认行为是结束进程,而我们不希望因为错误的写操作导致程序退出,引起SIGPIPE信号的写操作将设置errno为EPIPE;
SIGURG
在Linux环境下,内核通知应用程序带外数据到达主要有两种方法:
- I/O复用技术,select等系统调用在接收到带外数据时将返回,并向应用程序报告socket上的异常事件;
- 使用SIGURG信号;
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#define BUF_SIZE 1024
static int connfd;
void sig_urg( int sig )
{
int save_errno = errno;
char buffer[ BUF_SIZE ];
memset( buffer, '\0', BUF_SIZE );
int ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
printf( "got %d bytes of oob data '%s'\n", ret, buffer );
errno = save_errno;
}
void addsig( int sig, void ( *sig_handler )( int ) )
{
struct sigaction sa;
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset( &sa.sa_mask );
assert( sigaction( sig, &sa, NULL ) != -1 );
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( sock, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
addsig( SIGURG, sig_urg );
//使用SIGURG信号之前,必须设置socket的宿主进程或进程组
fcntl( connfd, F_SETOWN, getpid() );
char buffer[ BUF_SIZE ];
while( 1 )
{
memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
if( ret <= 0 )
{
break;
}
printf( "got %d bytes of normal data '%s'\n", ret, buffer );
}
close( connfd );
}
close( sock );
return 0;
}