10. 信号

第10章 信号

信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。Linux信号可由如下条件产生:

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
  • 系统异常。比如浮点异常和非法内存段访问。
  • 系统状态变化。比如alarm定时器到期将引起SIGALRM信号。
  • 运行kill命令或调用kill函数。

服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。

1. Linux信号概述

1.1 发送信号

Linux下,一个进程给其他进程发送信号的API是kill函数。其定义如下:

1
2
3
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
- 该函数把信号sig发送给目标进程; - 目标进程由pid参数指定,其可能的取值及含义如表10-1所示。

  • Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。

  • 但将sig设置为0可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。不过这种检测方式是不可靠的。

    • 一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程的PID;
    • 另一方面,这种检测方法不是原子操作。
  • 该函数成功时返回0,失败则返回-1并设置errno。几种可能的errno如表10-2所示。

1.2 信号处理方式

目标进程在收到信号时,需要定义一个接收函数来处理之。信号处理函数的原型如下:

1
2
#include<signal.h>
typedef void(*__sighandler_t)(int);
- 信号处理函数只带有一个整型参数,该参数用来指示信号类型。 - 信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。 - 除了用户自定义信号处理函数外,bits/signum.h头文件中还定义了信号的两种其他处理方式——SIG_IGN和SIG_DFL:

1
2
3
#include<bits/signum.h>
#define SIG_DFL((__sighandler_t)0)
#define SIG_IGN((__sighandler_t)1)
  • SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。
  • 信号的默认处理方式有如下几种:
    • 结束进程(Term)、
    • 忽略信号(Ign)、
    • 结束进程并生成核心转储文件(Core)、
    • 暂停进程(Stop),
    • 以及继续进程(Cont)。

1.3 Linux信号

  • Linux的可用信号都定义在bits/signum.h头文件中,其中包括标准信号和POSIX实时信号。

1.4 中断系统调用

如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。

  • 我们可以使用sigaction函数(见后文)为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
  • 对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如connect、epoll_wait)。POSIX没有规定这种行为,这是Linux独有的。

2. 信号函数

2.1 signal系统调用

要为一个信号设置处理函数,可以使用下面的signal系统调用:

1
2
#include<signal.h>
_sighandler_t signal(int sig,_sighandler_t _handler)
- sig参数指出要捕获的信号类型。 - _handler参数是_sighandler_t类型的函数指针,用于指定信号sig的处理函数。 - signal函数成功时返回一个函数指针,该函数指针的类型也是_sighandler_t。 - 这个返回值是前一次调用signal函数时传入的函数指针,或者是信号sig对应的默认处理函数指针SIG_DEF(如果是第一次调用signal的话)。 - signal系统调用出错时返回SIG_ERR,并设置errno。

2.2 sigaction系统调用

设置信号处理函数的更健壮的接口是如下的系统调用:

1
2
3
#include<signal.h>

int sigaction(int sig,const struct sigaction*act,struct sigaction*oact);
- sig参数指出要捕获的信号类型, - act参数指定新的信号处理方式, - oact参数则输出信号先前的处理方式(如果不为NULL的话)。act和oact都是sigaction结构体类型的指针,sigaction结构体描述了信号处理的细节,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct sigaction { 
#ifdef_USE_POSIX199309
union {
_sighandler_t sa_handler;
void(*sa_sigaction)(int,siginfo_t*,void*);
} _sigaction_handler;

#define sa_handler__sigaction_handler.sa_handler
#define sa_sigaction__sigaction_handler.sa_sigaction
#else
_sighandler_t sa_handler;
#endif
_sigset_t sa_mask;
int sa_flags;
void(*sa_restorer)(void);
};
  • 该结构体中的sa_hander成员指定信号处理函数。
  • sa_mask成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。
  • sa_mask是信号集sigset_t(_sigset_t的同义词)类型,该类型指定一组信号。
  • sa_flags成员用于设置程序收到信号时的行为,其可选值如表10-4所示。
  • sa_restorer成员已经过时,最好不要使用。
  • sigaction成功时返回0,失败则返回-1并设置errno。

3. 信号集

3.1 信号集函数

前文提到,Linux使用数据结构sigset_t来表示一组信号。其定义如下:

1
2
3
4
5
#include<bits/sigset.h> 
#define_SIGSET_NWORDS(1024/(8*sizeof(unsigned long int)))
typedef struct {
unsigned long int__val[_SIGSET_NWORDS];
} __sigset_t;
- sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符集fd_set类似。 - Linux提供了如下一组函数来设置、修改、删除和查询信号集:

1
2
3
4
5
6
#include<signal.h> 
int sigemptyset(sigset_t*_set)/*清空信号集*/
int sigfillset(sigset_t*_set)/*在信号集中设置所有信号*/
int sigaddset(sigset_t*_set,int_signo)/*将信号_signo添加至信号集中*/
int sigdelset(sigset_t*_set,int_signo)/*将信号_signo从信号集中删除*/
int sigismember(_const sigset_t*_set,int_signo)/*测试_signo是否在信号集中*/

3.2 进程信号掩码

前文提到,我们可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码。此外,如下函数也可以用于设置或查看进程的信号掩码:

1
2
#include<signal.h> 
int sigprocmask(int_how,_const sigset_t*_set,sigset_t*_oset);
- _set参数指定新的信号掩码, - _oset参数则输出原来的信号掩码(如果不为NULL的话)。 - 如果_set参数不为NULL,则_how参数指定设置进程信号掩码的方式,其可选值如表10-5所示。 - 如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获得进程当前的信号掩码。 - sigprocmask成功时返回0,失败则返回-1并设置errno。

3.3 被挂起的信号

  • 设置进程信号掩码后,被屏蔽的信号将不能被进程接收。
  • 如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。
  • 如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。

如下函数可以获得进程当前被挂起的信号集:

1
2
#include<signal.h> 
int sigpending(sigset_t*set);
  • set参数用于保存被挂起的信号集。显然,进程即使多次接收到同一个被挂起的信号,
  • sigpending函数也只能反映一次。并且,当我们再次使用sigprocmask使能该挂起的信号时,该信号的处理函数也只被触发一次。
  • sigpending成功时返回0,失败时返回-1并设置errno。

在多进程、多线程环境中,我们要以进程、线程为单位来处理信号和信号掩码。我们不能设想新创建的进程、线程具有和父进程、主线程完全相同的信号特征。比如,fork调用产生的子进程将继承父进程的信号掩码,但具有一个空的挂起信号集。

4. 统一事件源

信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。

信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)太久。

  • 一种典型的解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。
  • 信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。
  • 那么主循环怎么知道管道上何时有数据可读呢?这很简单,我们只需要使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源。
  • 很多优秀的I/O框架库和后台服务器程序都统一处理信号和I/O事件,比如Libevent I/O框架库和xinetd超级服务。代码清单10-1给出了统一事件源的一个简单实现。

代码清单10-1 统一事件源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#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 ) );
// 信号处理函数为sig_handler
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 );

ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
if( ret == -1 )
{
printf( "errno is %d\n", errno );
return 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);

// 使用socketpair创建管道,注册pipefd[0]上的可读事件
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
setnonblocking( pipefd[1] );
addfd( epollfd, pipefd[0] );

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;
}

5. 网络编程相关信号

5.1 SIGHUP

  • 当挂起进程的控制终端时,SIGHUP信号将被触发。
  • 对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。
  • 一个典型的例子是xinetd超级服务程序。
  • xinetd程序在接收到SIGHUP信号之后将调用hard_reconfig函数(见xinetd源码),它循环读取/etc/xinetd.d/目录下的每个子配置文件,并检测其变化。
  • 如果某个正在运行的子服务的配置文件被修改以停止服务,则xinetd主进程将给该子服务进程发送SIGTERM信号以结束它。
  • 如果某个子服务的配置文件被修改以开启服务,则xinetd将创建新的socket并将其绑定到该服务对应的端口上。

下面我们简单地分析xinetd处理SIGHUP信号的流程。测试机器Kongming20上具有如下环境:

1
2
3
4
5
6
7
8
9
$ps-ef|grep xinetd 
root 7438 1 0 11:32?00:00:00/usr/sbin/xinetd-stayalive-pidfile/var/run/xinetd.pid
root 7442 7438 0 11:32?00:00:00(xinetd service)echo-stream
Kongming20

$sudo lsof-p 7438 xinetd
7438 root 3r FIFO 0,8 0t0 37639 pipe xinetd
7438 root 4w FIFO 0,8 0t0 37639 pipe xinetd
7438 root 5u IPv6 37652 0t0 TCP*:echo(LISTEN)
  • 从ps的输出来看,xinetd创建了子进程7442,它运行echo-stream内部服务。
  • 从lsof的输出来看,xinetd打开了一个管道。
  • 该管道的读端文件描述符的值是3,写端文件描述符的值是4。
  • 后面我们将看到,它们的作用就是统一事件源。
  • 现在我们修改/etc/xinetd.d/目录下的部分配置文件,并给xinetd发送一个SIGHUP信号。具体操作如下:
1
2
3
$sudo sed-i's/disable.*=.*no/disable=yes/'/etc/xinetd.d/echo-stream#停止echo服务
$sudo sed-i's/disable.*=.*yes/disable=no/'/etc/xinetd.d/telnet#开启telnet服务
$sudo strace-p 7438&>a.txt $sudo kill-HUP xinetd

strace命令能跟踪程序执行时调用的系统调用和接收到的信号。

这里我们利用strace命令跟踪进程7438,即xinetd服务器程序,以观察xinetd是如何处理SIGHUP信号的。此次strace命令的部分输出如代码清单10-2所示。

代码清单10-2 用strace命令查看xinetd处理SIGHUP的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 信号处理
{
si_signo=SIGHUP, // 信号编号:挂起信号
si_code=SI_USER, // 信号代码:用户生成的信号
si_pid=7697, // 发送信号的进程ID
si_uid=0, // 发送信号的用户ID
si_value={int=1154706400,ptr=0x44d36be0} // 信号值
} (Hangup) // 挂起事件

// 系统调用
write(4,"\1",1)=1 // 向文件描述符4写入1个字节,返回1
sigreturn()=? // 信号返回,返回值未知
(mask now[]) // 当前信号掩码

// 轮询文件描述符
poll([{fd=5,events=POLLIN},
{fd=3,events=POLLIN}],2,-1)=1([{fd=3,revents=POLLIN}])
ioctl(3,FIONREAD,[1])=0 // 检查文件描述符3是否有数据可读,返回0

// 读取数据
read(3,"\1",1)=1 // 从文件描述符3读取1个字节,返回1

// 文件状态
stat64("/etc/xinetd.d/echo-stream", {st_mode=S_IFREG|0644,st_size=1149,...})=0 // 获取文件状态,成功

// 打开文件
open("/etc/xinetd.d/echo-stream",O_RDONLY)=8 // 以只读模式打开文件,返回文件描述符8

// 时间相关
time(NULL)=1337053896 // 获取当前时间戳

// 发送数据
send(7,"<31>May 15 11:51:36 xinetd[7438]"...,139,MSG_NOSIGNAL)=139 // 向文件描述符7发送数据,成功发送139字节

// 文件操作
fstat64(8,{st_mode=S_IFREG|0644,st_size=1149,...})=0 // 获取文件描述符8的状态,成功
lseek(8,0,SEEK_CUR)=0 // 移动文件描述符8的读/写文件偏移量至当前位置
fcntl64(8,F_GETFL)=0 // 获取文件描述符8的文件状态标志,返回0(flags O_RDONLY)
read(8,"#This is the configuration for"...,8192)=1149 // 从文件描述符8读取1149字节
read(8,"",8192)=0 // 尝试从文件描述符8读取数据,返回0(EOF)
close(8)=0 // 关闭文件描述符8

// 进程控制
kill(7442,SIGTERM)=0 // 向进程7442发送SIGTERM信号
waitpid(7442,NULL,WNOHANG)=0 // 等待进程7442结束,非阻塞

// 网络操作
socket(PF_INET6,SOCK_STREAM,IPPROTO_TCP)=5 // 创建IPv6 TCP套接字,返回文件描述符5
fcntl64(5,F_SETFD,FD_CLOEXEC)=0 // 设置文件描述符5的FD_CLOEXEC标志
setsockopt(5,SOL_IPV6,IPV6_V6ONLY,[0],4)=0 // 设置IPv6套接字选项,禁用IPv6-only模式
setsockopt(5,SOL_SOCKET,SO_REUSEADDR,[1],4)=0 // 设置套接字选项,允许地址重用
bind(5,{sa_family=AF_INET6,sin6_port=htons(23),inet_pton(AF_INET6,"::",&sin6_addr),sin6_flowinfo=0,sin6_scope_id=0},28)=0 // 绑定套接字至端口23
listen(5,64)=0 // 监听套接字,最大连接数64
该输出分为4个部分,我们用空行将每个部分隔开。

  • 第一部分描述程序接收到SIGHUP信号时,信号处理函数使用管道通知主程序该信号的到来。
    • 信号处理函数往文件描述符4(管道的写端)写入信号值1(SIGHUP信号),而主程序使用poll检测到文件描述符3(管道的读端)上有可读事件,就将管道上的数据读入。
  • 第二部分描述了xinetd重新读取一个子配置文件的过程。
  • 第三部分描述了xinetd给子进程echo-stream(PID为7442)发送SIGTERM信号来终止该子进程,并调用waitpid来等待该子进程结束。
  • 第四部分描述了xinetd启动telnet服务的过程:创建一个流服务socket并将其绑定到端口23上,然后监听该端口。

5.2 SIGPIPE

默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。

  • 我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。
  • 引起SIGPIPE信号的写操作将设置errno为EPIPE。

第5章提到,我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。

  • 在这种情况下,我们应该使用send函数反馈的errno值来判断管道或者socket连接的读端是否已经关闭。

此外,我们也可以利用I/O复用系统调用来检测管道和socket连接的读端是否已经关闭。

  • 以poll为例,当管道的读端关闭时,写端文件描述符上的POLLHUP事件将被触发;
  • 当socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发。

5.3 SIGURG

在Linux环境下,内核通知应用程序带外数据到达主要有两种方法:

  • 一种是第9章介绍的I/O复用技术,select等系统调用在接收到带外数据时将返回,并向应用程序报告socket上的异常事件,代码清单9-1给出了一个这方面的例子;
  • 另外一种方法就是使用SIGURG信号,如代码清单10-3所示。

代码清单10-3 用SIGURG检测带外数据是否到达

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#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_RESTART标志以自动重启被该信号中断的系统调用
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);
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;
}

编译并运行该服务器程序,然后使用代码清单5-6所描述的客户端程序来往该服务器程序发送数据,以观察服务器是如何同时处理普通数据和带外数据的。

TCP带外数据相关的所有知识。

  • 3.8节中我们介绍了TCP带外数据的基本知识,其中探讨了TCP模块是如何发送和接收带外数据的。
  • 5.8.1小节描述了如何在应用程序中使用带MSG_OOB标志的send/recv系统调用来发送/接收带外数据,并给出了相关代码。
  • 9.1.3小节和10.5.3小节分别介绍了检测带外数据是否到达的两种方法:
    • I/O复用系统调用报告的异常事件和SIGURG信号。
    • 但应用程序检测到带外数据到达后,我们还需要进一步判断带外数据在数据流中的具体位置,才能够准确无误地读取带外数据。
    • 5.9节介绍的sockatmark系统调用就是专门用于解决这个问题的。它判断一个socket是否处于带外标记,即该socket上下一个将被读取到的数据是否是带外数据。

10. 信号
http://binbo-zappy.github.io/2024/12/16/Linux高性能服务器编程-游双/10-信号/
作者
Binbo
发布于
2024年12月16日
许可协议