11. 定时器
网络程序需要处理的第三类事件是定时事件 ,比如定期检测一个客户连接的活动状态。如何有效的组织这些定时事件,使之能在预期的事件点被触发而且不影响服务器的主要逻辑,对于服务器的性能有者至关重要的影响。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间论,将所有定时器串联起来,以实现对定时时间的统一管理。 本章主要讨论两种高效管理定时器的容器:时间论和时间堆。
而定时是指在一段时间之后触发某段代码的机制,我们可以在这段代码中依此处理所有到期的定时器。Linux提供了三种定时方法:
- socket选项SO_RCVTIMEO和SO_SNDTIMEO
- SIGALRM信号
- I/O复用系统调用的超时参数
11.1 socket选项SO_RCVTIMEO和SO_SNDTIMEO
该选项分别用来设置socket接收数据超时时间和发送数据超时时间。因此,这两个选项仅对与数据接收和发送相关的socket专用系统调用有效。
系统调用 | 有效选项 | 系统调用超时后的行为 |
---|---|---|
send | SO_SNDTIMEO | 返回-1,设置errno为EAGAIN或EWOUDLBLOCK |
sendmsg | SO_SNDTIMEO | 返回-1,设置errno为EAGAIN或EWOUDLBLOCK |
recv | SO_REVTIMEO | 返回-1,设置errno为EAGAIN或EWOUDLBLOCK |
revmsg | SO_REVTIMEO | 返回-1,设置errno为EAGAIN或EWOUDLBLOCK |
accept | SO_REVTIMEO | 返回-1,设置errno为EAGAIN或EWOUDLBLOCK |
connect | SO_SNDTIMEO | 返回-1,设置errno为EINPROGRESS |
由此,我们可以根据系统调用的返回值以及errno来判断系统调用超时时间是否已到,进而据欸的那个是否开始处理定时任务。
ini
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int timeout_connect( const char* ip, int port, int time )
{
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 sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
/*设置时间的timeval结构体*/
struct timeval timeout;
/*tv_sec是秒数,tv_usec是微秒数*/
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof( timeout );
ret = setsockopt( sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len );
assert( ret != -1 );
ret = connect( sockfd, ( struct sockaddr* )&address, sizeof( address ) );
if ( ret == -1 )
{
if( errno == EINPROGRESS )
{
printf( "connecting timeout\n" );
return -1;
}
printf( "error occur when connecting to server\n" );
return -1;
}
return sockfd;
}
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 sockfd = timeout_connect( ip, port, 10 );
if ( sockfd < 0 )
{
return 1;
}
return 0;
}
11.2 SIGALRM信号
由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。 但是当处理多个定时任务,我们就需要不断地触发SIGALRM信号,并在其信号处理函数中执行到期的任务。
先通过一个实例------处理非活动连接,来介绍如何使用SIGALRM信号定时。
11.2.1 基于升序链表的定时器
定时器至少包括两个成员:一个超时时间(相对时间或者绝对时间)和一个任务回调函数。
ini
#ifndef LST_TIMER
#define LST_TIMER
#include <time.h>
#define BUFFER_SIZE 64
class util_timer; /*前向声明*/
/*
用户数据结构:客户端socket地址、socket文件描述符、读缓存和定时器
*/
struct client_data
{
sockaddr_in address;
int sockfd;
char buf[ BUFFER_SIZE ];
util_timer* timer;
};
/*定时器链表类*/
class util_timer
{
public:
util_timer() : prev( NULL ), next( NULL ){}
public:
/*任务的超时时间,这里使用绝对时间*/
time_t expire;
/*函数指针,指向回调函数*/
void (*cb_func)( client_data* );
/*回调函数处理的客户数据,由定时器的执行者传递给回调函数*/
client_data* user_data;
util_timer* prev;
util_timer* next;
};
/*定时器链表,一个升序双向链表,且带有头节点和尾节点*/
class sort_timer_lst
{
public:
sort_timer_lst() : head( NULL ), tail( NULL ) {}
/*析构函数,删除所有节点*/
~sort_timer_lst()
{
util_timer* tmp = head;
while( tmp )
{
head = tmp->next;
delete tmp;
tmp = head;
}
}
/*为有序链表添加节点*/
void add_timer( util_timer* timer )
{
if( !timer )
{
return;
}
/*链表为空的情况*/
if( !head )
{
head = tail = timer;
return;
}
/*插入首位*/
if( timer->expire < head->expire )
{
timer->next = head;
head->prev = timer;
head = timer;
return;
}
/*插入位置不是首位,调用重载函数寻找位置并插入*/
add_timer( timer, head );
}
/*当某个定时器任务时间延长,则调整位置*/
void adjust_timer( util_timer* timer )
{
if( !timer )
{
return;
}
util_timer* tmp = timer->next;
/*如果被调整的目标位于定时器尾部或者调整后的超时值仍然小于下一个定时器的超时值,则不调整*/
if( !tmp || ( timer->expire < tmp->expire ) )
{
return;
}
if( timer == head )
{
head = head->next;
head->prev = NULL;
timer->next = NULL;
add_timer( timer, head );
}
else
{
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
add_timer( timer, timer->next );
}
}
void del_timer( util_timer* timer )
{
if( !timer )
{
return;
}
if( ( timer == head ) && ( timer == tail ) )
{
delete timer;
head = NULL;
tail = NULL;
return;
}
if( timer == head )
{
head = head->next;
head->prev = NULL;
delete timer;
return;
}
if( timer == tail )
{
tail = tail->prev;
tail->next = NULL;
delete timer;
return;
}
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
delete timer;
}
void tick()
{
if( !head )
{
return;
}
printf( "timer tick\n" );
/*从头节点开始依次处理每个定时器,和系统当前时间比较,即可确定是否超时*/
time_t cur = time( NULL );
util_timer* tmp = head;
while( tmp )
{
if( cur < tmp->expire )
{
break;
}
tmp->cb_func( tmp->user_data );
head = tmp->next;
if( head )
{
head->prev = NULL;
}
delete tmp;
tmp = head;
}
}
private:
void add_timer( util_timer* timer, util_timer* lst_head )
{
util_timer* prev = lst_head;
util_timer* tmp = prev->next;
while( tmp )
{
if( timer->expire < tmp->expire )
{
prev->next = timer;
timer->next = tmp;
tmp->prev = timer;
timer->prev = prev;
break;
}
prev = tmp;
tmp = tmp->next;
}
if( !tmp )
{
prev->next = timer;
timer->prev = prev;
timer->next = NULL;
tail = timer;
}
}
private:
util_timer* head;
util_timer* tail;
};
11.2.2 处理非活动连接
服务器程序通常要定期处理非活动连接:给客户端发一个重连请求,或者关闭该连接,或者其它。虽然Linux在内核中提供了对连接是否处于活动状态的定期检查机制,我们可以通过socket选项KEEPALIVE来激活它。不过该方式会使得对于连接的管理变复杂。我们可以通过alarm函数周期性地触发SIGALRM信号,该信号地信号处理函数利用管道通知主循环执行定时器链表上的定时任务------关闭非活动的连接。
13. 多进程编程
- 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用;
- 僵尸进程以及如何避免僵尸进程;
- 进程间通信最简单的方式:管道;
- 3种System V进程间通信方式:信号量、消息队列和共享内存;
- 在进程间传递文件描述符的通用方法:通过UNIX本地域socket传递特殊的辅助数据。
13.1 fork系统调用
arduino
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
该函数每次调用都返回两次,在父进程种返回子进程的ID,在子进程种则返回0。 fork调用失败则返回-1,并设置errno。
13.2 exec系列系统调用
执行其它程序,即替换当成进程映像。就需要使用如下exec系列函数之一:
arduino
#include <unistd.h>
extern char** environ;
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char*path, const char* arg, ...);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execve(const char*path, char* const argv[], char* const envp[]);
- path参数设置可执行文件的完整路径;
- file参数可以接收文件名;
- arg接受可变参数,argv接受参数数组,它们会传递给新程序的main函数;
- envp参数用于设置新程序的环境变量,如未设置,则使用全局变量environ指定的环境变量。
一般情况下,exec函数是不返回的,除非出错。它出错时返回-1,并设置errno。如果没出错,则源程序种的exec调用之后的代码都不会执行,因为此时原程序已经被exec的参数指定的程序完全替换。
13.3 处理僵尸进程
对于多进程程序而言,父进程一般需要追踪子进程的退出状态。因此,当子进程结束运行时,内核一般不会立即释放该进程的进程表项,以满足父进程后续对该子进程退出信息的查询。在子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程为僵尸进程。 另外一种使子进程进入僵尸态的情况是:父进程结束或异常终止,而子进程继续运行。此时,init进程接管了该子进程,并等待他结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。
当父进程没有正确地处理子进程返回信息的时候,子进程都将停留在僵尸态,并占据内核资源。下面这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生。
arduino
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
pid_t waitpid(pid_t pid, int* stat_loc, int options);
- wait函数将阻塞进程,直到进程的某个子进程结束运行为止。它返回结束运行的子进程的PID,并将该子进程的退出信息保存到stat_loc中;
- waitpid只等待pid参数指定的子进程(如果pid为-1,则和wait相同),stat_loc和wait相同,options参数控制waitpid函数的行为(常用取值使WNOHANG,非阻塞模式)。
之前提到,要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。 父进程可以通过捕获SIGCHLD信号得知子进程已结束,并在信号处理函数中调用waitpid函数以彻底结束一个子进程。
arduino
static void handle_child(int sig) {
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
/*对结束的子进程进行善后处理*/
}
}
关于僵尸进程的实验:
arduino
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main () {
pid_t pid;
pid = fork();
if(pid == 0) {
printf("This is chind pid: %d\n", getpid());
exit(0);
}
else {
/*在子进程退出后,父进程等待30后才执行wait操作,此时子进程就变成了僵尸进程*/
printf("This is parent pid: %d\n", getpid());
printf("child pid is: %d\n", pid);
sleep(30);
int stat;
wait(&stat);
}
}
13.4 管道
pipe创建的的管道只能用于有关联的两个进程传递消息;
arduino
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
int main () {
pid_t pid;
int fd[2];
int ret = pipe(fd);
assert(ret != -1);
pid = fork();
/*父进程*/
if(pid == 0) {
close(fd[0]);
char mesg[] = "hello, world";
int len = write(fd[1], mesg, strlen(mesg));
if(len != 0) {
printf("message send success!\n");
}
}
else {
close(fd[1]);
char buf[1024];
int len = read(fd[0], buf, 1024);
if(len != -1) {
printf("read data is: %s, len:%d\n", buf, len);
}
}
}
socketpair提供了一个创建全双工管道的系统调用,也只能用于父子进程。
特殊管道FIFO也叫命名管道,可以用于无关联进程之间的通信。
arduino
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
char buf[88];
int fd;
char pipifile[] = "/tmp/fifo.pipe";
mkfifo(pipifile, 0777);
if(fork() > 0) {
char s[] = "hello!";
fd = open(pipifile, O_WRONLY);
write(fd, s, strlen(s));
}
else {
fd = open(pipifile, O_RDONLY);
read(fd, buf, strlen(buf));
printf("the message from the pipe is: %s\n", buf);
}
}
13.5 信号量
Linux信号量的API定义在sys/sem.h头文件中,主要包含3个系统调用:semget、semop和semctl。它们被设置为操作一组信号量,即信号量集,而不是单个信号量。
13.5.1 semget系统调用
semget系统调用创建一个新的信号集,或者获取一个已经存在的信号量集。
arduino
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
- key参数是一个键值,用来标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量;
- num_sems指定要创建/获取的信号量集中信号量的数目;
- sem_flags参数指定一组标志。它底端的9个比特是该信号量的权限。
semget成功时返回一个正整数值,它是信号量集的标识符。
如果semget用于创建信号量集,则与之关联的内核数据结构体semid_ds将被创建并初始化。semid_ds结构体定义如下:
arduino
#include <sys/sem.h>
/*该结构体用于描述IPC对象(信号量、共享内存和消息队列)的权限*/
struct ipc_perm {
key_t key; /*键值*/
uid_t uid;
gid_t gid;
uid_t cuid;
git_t cgid;
mode_t mode; /*访问权限*/
/*省略其它填充字段*/
}
struct semid_ds {
struct ipc_perm sem_perm; /*信号量的操作权限*/
unsigned long int sem_nsems; /*信号数目*/
time_t sem_otime; /*最后依次调用semop的时间*/
time_t sem_ctime; /*最后一次调用semctl的时间*/
}
13.5.2 semop系统调用
semop系统调用改变信号量的值,即实行P、V操作。
semop操作实际是对一些内核变量的操作:
arduino
unsigned short semval; /*信号量的值*/
unsigned short semzcnt; /*等待信号量变为0的进程数量*/
unsigned short semncnt; /*等待信号量增加的进程数量*/
pid_t sempid; /*最后一次执行semop操作的进程ID*/
arduino
#include <sys/sem.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
-
sem_id是由semget调用返回的信号量集标识符;
-
sem_ops参数指向一个sembuf结构体类型数组:
arduinostruct sembuf { unsigned short int sem_num; /*指定信号量集中信号量的编号*/ short int sem_op; /*sem_op指定操作类型,其可选值为正整数、0和负整数*/ short int sem_flg; /*可选值包括IPC_NOWAIT-非阻塞,SEM_UNDO进程退出时取消正在进行的semop操作*/ }
-
num_sem_ops指定要执行的操作个数。
13.5.3 semctl系统调用
semctl系统调用允许调用者对信号量进行直接控制。定义如下:
arduino
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
-
sem_id是由semget调用返回的信号量集标识符;
-
sem_num指定被操作的信号量在信号量集中的编号;
-
command参数指定要执行的命令,有的命令还需要传递第四个参数。由用户自定义,但推荐格式如下:
cunion semun { int val; /*用于SETVAL命令*/ struct semid_ds* buf; /*用于IPC_STAT和IPC_SET命令*/ unsigned short* array; /*用于GETALL和SETALL命令*/ struct seminfo* __buf; /*用于IPC_INFO命令*/ }
semctl支持的所有命令如下:
命令 含义 SEMCTL成功时的返回值 IPC_STAT 将信号量集关联的内核数据结构复制到semun.buf中 0 IPC_SET 将semun.buf中的部分成员复制到信号量集关联的内核数据结构中,同时内核数据中的semid_ds.sem_ctime被更新 0 IPC_RMID 立即移除信号量集,唤醒所有等待该信号量集的进程 0 SETALL 用semun.array中的数据填充由sem_id标识的信号量集中的所有信号量的semval值,同时内核数据中的semid_ds.sem_ctime被更新 0 SETVAL 将信号量集的semval值设置为semun.val,同时内核数据中的semid_ds.sem_ctime被更新 0
13.5.4 特殊键值IPC_PRIVATE
semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE(其值为0),这样无论信号量是否存在,semget都将创建一个新的信号量。
c
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
union semun
{
int val;
struct semid_ds* buf;
unsigned short int* array;
struct seminfo* __buf;
};
/*每次对信号量集中的0号信号量执行op操作*/
void pv( int sem_id, int op )
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = op;
sem_b.sem_flg = SEM_UNDO;
semop( sem_id, &sem_b, 1 );
}
int main( int argc, char* argv[] )
{
/*创建一个新的信号集*/
int sem_id = semget( IPC_PRIVATE, 1, 0666 );
union semun sem_un;
sem_un.val = 1;
/*将信号量集中的0号信号量设置为1*/
semctl( sem_id, 0, SETVAL, sem_un );
pid_t id = fork();
if( id < 0 )
{
return 1;
}
else if( id == 0 )
{
printf( "child try to get binary sem\n" );
pv( sem_id, -1 );
printf( "child get the sem and would release it after 5 seconds\n" );
sleep( 5 );
pv( sem_id, 1 );
exit( 0 );
}
else
{
printf( "parent try to get binary sem\n" );
pv( sem_id, -1 );
printf( "parent get the sem and would release it after 5 seconds\n" );
sleep( 5 );
pv( sem_id, 1 );
}
waitpid( id, NULL, 0 );
semctl( sem_id, 0, IPC_RMID, sem_un );
return 0;
}
13.6 共享内存
共享内存是最高效的IPC机制,因为不涉及进程之间的任何数据传输。但需要使用其它辅助手段来同步进程对共享内存的访问,否则会产生竞态条件 。Linux共享内存的API都定义在sys/shm.h
头文件中,包括4个系统调用:shmget、shmat、shmdt和shmctl。
13.6.1 shmget系统调用
shmget
系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。
arduino
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
-
key参实是一个键值,用来标识一段全局唯一的共享内存;
-
size参数指定共享内存的大小,单位是字节;
-
shmflg参数的使用和semget系统调用的sem_flags参数相同。不过它支持两个额外标志------SHM_HUGETLB和SHM_NORESERVE。
- SHM_HUGETLB,类似于mmap的MAP_HUGETLB标志,系统将使用"大页面"来为共享内存分配空间。
- SHM_NORESERVE,类似于mmap的MAP_NORESERVE标志,不为共享内存保留交换分区。当物理内存不足时,对该共享内存执行写操作将触发SIGSEGV信号。
shmget成功时返回一个正整数值,失败时返回-1。
如果shmget用于创建共享内存,则这段内存的所有字节都被初始化为0,与之关联的内核数据接哦古shmid_ds将被创建并初始化。
arduino
struct shmid_ds {
struct ipc_perm shm_perm; /*共享内存的操作权限*/
size_t shm_segsz; /*共享内存大小,单位是字节*/
__time_t shm_atime; /*对这段内存最后一次调用shmat的时间*/
__time_t shm_dtime; /*对这段内存最后一次调用shmdt的时间*/
__time_t shm_ctime; /*对这段内存最后一次调用shmct的时间*/
}
13.6.2 shmat和shmdt系统调用
共享内存被创建/获取之后,我们不能理解访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存后,也需要将它从进程地址空间中分离。 这两项任务由以下两个系统调用实现:
arduino
#include <sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);
int shmdt(const void* shm_addr);
13.6.3 shmctl系统调用
shmctl系统调用控制共享内存的某些属性。定义如下:
arduino
#include <sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds* buf);
13.6.4 共享内存的POSIX方法
使用共享内存的聊天室服务器程序
ini
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#define USER_LIMIT 5
#define BUFFER_SIZE 1024
#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define PROCESS_LIMIT 65536
struct client_data
{
sockaddr_in address;
int connfd;
pid_t pid;
int pipefd[2];
};
static const char* shm_name = "/my_shm";
int sig_pipefd[2];
int epollfd;
int listenfd;
int shmfd;
char* share_mem = 0;
client_data* users = 0;
int* sub_process = 0;
int user_count = 0;
bool stop_child = false;
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( sig_pipefd[1], ( char* )&msg, 1, 0 );
errno = save_errno;
}
void addsig( int sig, void(*handler)(int), bool restart = true )
{
struct sigaction sa;
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = handler;
if( restart )
{
sa.sa_flags |= SA_RESTART;
}
sigfillset( &sa.sa_mask );
assert( sigaction( sig, &sa, NULL ) != -1 );
}
void del_resource()
{
close( sig_pipefd[0] );
close( sig_pipefd[1] );
close( listenfd );
close( epollfd );
shm_unlink( shm_name );
delete [] users;
delete [] sub_process;
}
void child_term_handler( int sig )
{
stop_child = true;
}
int run_child( int idx, client_data* users, char* share_mem )
{
epoll_event events[ MAX_EVENT_NUMBER ];
int child_epollfd = epoll_create( 5 );
assert( child_epollfd != -1 );
int connfd = users[idx].connfd;
addfd( child_epollfd, connfd );
int pipefd = users[idx].pipefd[1];
addfd( child_epollfd, pipefd );
int ret;
/*终止进程信号则停止子进程的死循环*/
addsig( SIGTERM, child_term_handler, false );
while( !stop_child )
{
int number = epoll_wait( child_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 == connfd ) && ( events[i].events & EPOLLIN ) )
{
memset( share_mem + idx*BUFFER_SIZE, '\0', BUFFER_SIZE );
ret = recv( connfd, share_mem + idx*BUFFER_SIZE, BUFFER_SIZE-1, 0 );
if( ret < 0 )
{
if( errno != EAGAIN )
{
stop_child = true;
}
}
else if( ret == 0 )
{
stop_child = true;
}
else
{
/*成功读取客户数据之后通知主进程来处理*/
send( pipefd, ( char* )&idx, sizeof( idx ), 0 );
}
}
/*主进程通知本进程(通过管道)将client个客户的数据发送到本进程负责的客户端*/
else if( ( sockfd == pipefd ) && ( events[i].events & EPOLLIN ) )
{
int client = 0;
ret = recv( sockfd, ( char* )&client, sizeof( client ), 0 );
if( ret < 0 )
{
if( errno != EAGAIN )
{
stop_child = true;
}
}
else if( ret == 0 )
{
stop_child = true;
}
else
{
send( connfd, share_mem + client * BUFFER_SIZE, BUFFER_SIZE, 0 );
}
}
else
{
continue;
}
}
}
close( connfd );
close( pipefd );
close( child_epollfd );
return 0;
}
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 );
listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
user_count = 0;
users = new client_data [ USER_LIMIT+1 ];
sub_process = new int [ PROCESS_LIMIT ];
for( int i = 0; i < PROCESS_LIMIT; ++i )
{
sub_process[i] = -1;
}
epoll_event events[ MAX_EVENT_NUMBER ];
epollfd = epoll_create( 5 );
assert( epollfd != -1 );
/*为监听socket设置ET模式,且fd为非阻塞*/
addfd( epollfd, listenfd );
/*双向通信*/
ret = socketpair( PF_UNIX, SOCK_STREAM, 0, sig_pipefd );
assert( ret != -1 );
/*把写入端设置为非阻塞,读取端放到事件监听集合中*/
setnonblocking( sig_pipefd[1] );
addfd( epollfd, sig_pipefd[0] );
/*子进程状态发生变化*/
addsig( SIGCHLD, sig_handler );
/*终止进程*/
addsig( SIGTERM, sig_handler );
/*键盘输入中断进程*/
addsig( SIGINT, sig_handler );
/*socket连接中有数据*/
addsig( SIGPIPE, SIG_IGN );
bool stop_server = false;
bool terminate = false;
/*以可读可写的方式打开一个共享内存对象*/
shmfd = shm_open( shm_name, O_CREAT | O_RDWR, 0666 );
assert( shmfd != -1 );
/*修改共享内存大小*/
ret = ftruncate( shmfd, USER_LIMIT * BUFFER_SIZE );
assert( ret != -1 );
share_mem = (char*)mmap( NULL, USER_LIMIT * BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shmfd, 0 );
assert( share_mem != MAP_FAILED );
close( shmfd );
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 );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
continue;
}
/*超出限制则关闭新建立的连接*/
if( user_count >= USER_LIMIT )
{
const char* info = "too many users\n";
printf( "%s", info );
send( connfd, info, strlen( info ), 0 );
close( connfd );
continue;
}
/*users数组中记录IP地址,socket连接以及和父进程通信用的管道*/
users[user_count].address = client_address;
users[user_count].connfd = connfd;
/*在父进程和子进程之间建立管道*/
ret = socketpair( PF_UNIX, SOCK_STREAM, 0, users[user_count].pipefd );
assert( ret != -1 );
pid_t pid = fork();
if( pid < 0 )
{
close( connfd );
continue;
}
else if( pid == 0 )
{
/*为什么要关闭子进程的sig_pipefd*/
close( epollfd );
close( listenfd );
close( users[user_count].pipefd[0] );
close( sig_pipefd[0] );
close( sig_pipefd[1] );
/*运行子进程*/
run_child( user_count, users, share_mem );
/*为什么要删除子进程的映射?答:子进程运行结束后就删除*/
munmap( (void*)share_mem, USER_LIMIT * BUFFER_SIZE );
exit( 0 );
}
else
{
close( connfd );
close( users[user_count].pipefd[1] );
addfd( epollfd, users[user_count].pipefd[0] );
users[user_count].pid = pid;
sub_process[pid] = user_count;
user_count++;
}
}
/*有信号发生*/
else if( ( sockfd == sig_pipefd[0] ) && ( events[i].events & EPOLLIN ) )
{
int sig;
char signals[1024];
ret = recv( sig_pipefd[0], signals, sizeof( signals ), 0 );
if( ret == -1 )
{
continue;
}
else if( ret == 0 )
{
continue;
}
else
{
for( int i = 0; i < ret; ++i )
{
switch( signals[i] )
{
case SIGCHLD:
{
pid_t pid;
int stat;
while ( ( pid = waitpid( -1, &stat, WNOHANG ) ) > 0 )
{
int del_user = sub_process[pid];
sub_process[pid] = -1;
if( ( del_user < 0 ) || ( del_user > USER_LIMIT ) )
{
printf( "the deleted user was not change\n" );
continue;
}
epoll_ctl( epollfd, EPOLL_CTL_DEL, users[del_user].pipefd[0], 0 );
close( users[del_user].pipefd[0] );
users[del_user] = users[--user_count];
sub_process[users[del_user].pid] = del_user;
printf( "child %d exit, now we have %d users\n", del_user, user_count );
}
if( terminate && user_count == 0 )
{
stop_server = true;
}
break;
}
case SIGTERM:
case SIGINT:
{
printf( "kill all the clild now\n" );
//addsig( SIGTERM, SIG_IGN );
//addsig( SIGINT, SIG_IGN );
if( user_count == 0 )
{
stop_server = true;
break;
}
for( int i = 0; i < user_count; ++i )
{
int pid = users[i].pid;
kill( pid, SIGTERM );
}
terminate = true;
break;
}
default:
{
break;
}
}
}
}
}
else if( events[i].events & EPOLLIN )
{
int child = 0;
ret = recv( sockfd, ( char* )&child, sizeof( child ), 0 );
printf( "read data from child accross pipe\n" );
if( ret == -1 )
{
continue;
}
else if( ret == 0 )
{
continue;
}
else
{
for( int j = 0; j < user_count; ++j )
{
if( users[j].pipefd[0] != sockfd )
{
printf( "send data to child accross pipe\n" );
send( users[j].pipefd[0], ( char* )&child, sizeof( child ), 0 );
}
}
}
}
}
}
del_resource();
return 0;
}
15.1 Linux上的五种I/O模型
15.1.1 阻塞I/O
调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必 须等这个函数返回才能进行下一步动作。
15.1.2 非阻塞I/O
非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调 用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两 种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成 EAGAIN。
15.1.3 I/O多路复用
Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是 这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数 据可读或可写时,才真正调用IO操作函数。
15.1.4 信号驱动I/O
Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进 程收到SIGIO 信号,然后处理 IO 事件。
内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需 要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。
15.1.5 异步
Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方 式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。