使用
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数1:我们要进行处理的信号。命令kill -l查看(共64个),这些信号都是系统定义的宏。
参数2:我们处理的方式(3种)。
第一种:
signal(SIGINT, SIG_ING);
//SIG_ING表示忽略指定信号
//SIGINT信号通常是CTRL+C,或DELETE。
第二种:
signal(SIGINT, SIG_DFL); //SIG_DFL执行默认系统动作
第三种:
signal(SIGINT, void(*handle)(int));
example:
void handler(int sig)
{
std::cout << "捕捉到信号" << sig << std::endl;
}
int main()
{
signal(SIGALRM, handler);
return 0;
}
常用信号:
SIGABRT
由调用abort函数产生,进程非正常退出
SIGALRM
用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS
由内存访问引起的某种特定的硬件异常
SIGFPE
数学相关的异常,如被0除,浮点溢出,等等
信号机制
首先明确内核负责接收信号,然后将其放入对应进程的信号队列。此时信号在内核空间,进程在用户空间。然后内核向进程发送一个中断,使其切到内核态。
而后进程在两种情况下对信号进行检测:
* 进程处理完中断从内核返回用户态前。
* 进程在内核态,从睡眠到被唤醒时检测。
检测到信号后,将当前内核栈内容拷贝到用户栈,并让下一条栈上指令指向信号处理函数。然后切回用户态,处理函数。
信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。确定处理完,恢复中断前的运行位置,然后回到用户态继续执行。
信号使用
发送信号:
int raise(int sig); //想当前进程发送sig信号
int kill(pid_t pid, int sig); //向进程pid发送信号sig
//pid > 0 :向进程号为pid的进程发送信号
//pid = 0 :向当前进程所在的进程组发送信号
//pid = -1 :向所有进程(除PID=1外)发送信号(权限范围内)
//pid < -1 :向进程组号为-pid的所有进程发送信号
将进程睡眠直到接收信号:
int pause(void); //将进程转入睡眠状态,直到接收到任意信号
int sigsuspend(const sigset_t* mask); //将进程睡眠直到接收到特定信号
信号驱动IO
除了多线程,多路复用,用信号驱动IO也能实现文件描述符上IO事件的管理。
原理:
如果想关注某文件描述符上的IO操作,就请求内核为自己发送一个信号,之后进程就可以执行其他任务直到I/O就绪为止。此时内核会向进程发送信号。
使用步骤:
- 设置文件描述符属主。即如果有事件通知给谁,一般是本进程。
- 设置文件描述符非阻塞。
- 通过打开O_ASYNC标志使信号驱动I/O。
- 为内核发送的通知信号安装一个信号处理例程。
代码:
void setsocket(int fd)
{
fcntl(fd, F_SETOWN, getpid()); //当发生IO时通知此进程
int flag = fcntl(fd, F_GETFL); //取得文件描述符状态旗标
fcntl(fd, F_SETFL, flag | O_ASYNC | O_NONBLOCK); //O_ASYNC使信号驱动IO
}
void handler(int sig)
{
printf("IO事件通知\n");
}
还有,信号驱动I/O只支持边缘触发通知,所以处理可读事件要循环read()直到返回错误码EAGAIN或EWOULDBLOCK。
那么IO信号什么情况会被发送?
- 监听套接字接收到新的连接。
- TCP主动端请求连接完成,进入ESTABLISHED状态。
- 套接字有新的事件。(注意不只是可读事件,旧事件没读完不会二次触发)
- 套接字对端使用shutdouwn()关闭写连接,或者close()。
- 套接字上发送缓冲区有新的空间。
- 套接字发生异步错误。
代码时发现一些细节:
shutdown(sockfd, SHUT_RD); //不会触发IO信号
close(sockfd); //触发
shutdown(sockfd, SHUT_WR); //触发IO信号
close(sockfd); //不触发
这个感觉跟epoll有点像了,也是记住文件描述符,就绪通知,而且自我感觉这个比epoll更棒,因为epoll还需要阻塞epoll_wait()去拿到那些套接字,同epoll一样的,程序性能不会因为检查的文件描述符个数而下降。
但现在有个问题:
默认的IO就绪通知信号SIGIO是个非实时信号。这就尴尬了,以程序的运行速度,很容易发生多个信号合成一个信号的情况(事实上我测试程序时它就无比眼疾手快的合了),测试时我用的sleep()来观察,实际中当然不行,所以必须改一些设置。还有既然要求实用性,刚刚只能接收一个参数的signal函数已经不能满足要求了。
- 通过专属Linux的fcntl() F_SETSIG操作来指定一个实时信号,取代SIGIO。
- 信号处理函数使用sigaction()来安装。
来看一下函数原型:
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
他的参数比较复杂,但足够传入发生事件描述符,活跃事件类型等信息,到这里,基本已经能够完成服务器监听套接字并处理活跃事件的功能了。
还有个小细节:排队的实时信号可排队,但是队长有限,超出会被丢弃,这个一个可以通过系统设置来改,一个可以通过sigwaitinfo()函数获取,然后临时切换到select()等去处理,恩,听起来就难得不要不要。