服务器编程的初步探索(定时,IO复用,信号统一事件源)

服务器编程的初步探索:

服务器程序通常需要处理的三类事件:I/O事件,信号及定时事件。有两种事件的处理模式:
Reactor模式:要求主线程(IO处理单元)只负责监听文件描述符上是否有事件发生(可读可写),若有,则立即通知工作线程(逻辑单元),将socket可读可写的时间放到可读可写的请求队列中,交给工作工作线程处理。
Rroactor模式:将所有的IO操作都交给主线程和内核来处理(读,写),工作线程仅仅负责处理逻辑,如主线程读完之后user[sockfd].read(),选择一个工作线程来处理客户的请求pool->append(user+sockfd).

通常会使用同步IO模型(如epoll_wait)实现Reactor,使用异步IO(aio_read和aio_write)实现实现Proactor。
为什么同步IO也可以模拟的Proactor事件处理模式?
同步(阻塞)IO:在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件,发送网络数据时,就需要等待IO操作完成,才能继续下一步的操作。这种称为同步IO。
异步I(非阻塞)IO:当代码需要执行一个耗时的IO操作的时候,它只需要发出IO指令,并不用等待IO的结果,然后就可以执行其他的代码。一段时间过后,当IO操作返回的时候,在通知CPU做处理。

IO复用的方式:epoll,select, poll
对于select和poll来说,所有的文件描述符都是在用户态被加入到其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态。epoll则将整个文件描述集合维护在内核态,每次添加文件描述符的时候都需要执行一个系统调用。系统调用的开销很大,而且短期活跃连接的请求下,epoll可能会慢比起select和poll由于大量的系统调用开销。
select使用的是线性表描述的文件描述符集合,文件描述符有上限;poll使用链表来描述;epoll底层通过红黑树来描述,并且维护一个ready list,将事件表中已经就绪的事件调价到这里,在使用epoll_wait调用的时候,仅仅观察这个list又没没有数据。
select和poll的最大的开销是来时内核判断有无文件描述符处于就绪这一过程;每次执行select和poll调用时,都会采用遍历的方式,遍历整个文件描述集合去判断各个文件描述符有无活动;epoll则不需要这种检查判断,当有活动产生的时候,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符存放到ready list中等待epoll_wait调用后被处理。
select和poll都只能工作在LT模式下,而同时可以支持ET和LT模式。

以上,当监听的fd数量较少的时候,且fd都很活跃的情况下,可以使用select和poll;当监听到的fd数量较多,且单位事件仅部分活跃的情况下,使用epoll会提升性能。

epoll_wait在调用的时候,内核会发生什么?在(LT ,ET)两种模式下。
LT:类似于select,LT回去遍历epoll事件表中的每一个的文件描述符,来观察是否有感兴趣的事件发生,如果有(触发了该文件描述符上的回调函数),epoll_wait就会以非阻塞的方式返回。若epoll事件没有被处理完(没有返回EWOULDBLOCK),该事件还会还会被后续的epoll_wait再次触发。
ET:ET在发生感兴趣的事件发生后,立即返回,并且sleep这一事件的epoll_wait,不管该事件有没有结束。
在使用ET模式时候,必须要保证该文件描述符都是非阻塞的(确保咋没有数据可读的时候,该文件描述符不会一直阻塞);并且每次调用read和write的时候都必须等到她们她们返回EWOULDBLOCK(确保所有的数据都已经读完或是写完)。

GET 和 POST的区别
最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。
GET请求参数会被完整的保留在浏览器的历史记录中,而POST中的参数不会被保留。
GET请求在URL中传递的参数是有长度的限制的。(大多数)浏览器通常都会限制URL长度在2K个字节,而大多数服务器最多处理64K大小的url
GET产生一个TCP数据包;POST产生两个数据包。对于GET方式的请求,浏览器会把http header 和 data一并发送出去,服务器相应200(返回数据);对于POST,浏览器先发送header,服务器相应100(表示信息已经接受,继续处理),浏览器再发送data,服务器响应200OK(返回数据)。

单个数据库连接时如何实现的?
1.使用mysql_init()初始化连接
2.使用mysql_real_connect()建立一个到mysql数据库的连接
3.使用mysql_query()执行查询语句
4.使用result = mysql_store_result(mysql)获取结果集
5.使用mysql_num_fields(result)获取查询的列数,mysql_num_rows(result)获取结果集的行数
6.使用mysql_fetch_row(result)不断的获取下一行,然后循环输出
7.使用mysql_free_result(result)释放结果集所占用的内存
8.使用mysql_close(conn)关闭连接。

定时器
非活跃:是指客户端与服务器建立连接之后,长时间的不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。
定时事件:是指固定一段时间之后触发某段代码,是由该代码处理一件事情,如从内核时间表删除事件,并关闭文件描述符,释放连接资源的浪费。
定时器:是指利用结构体或是其他的形式,将多种定时的事件封装起来。
定时器容器:是指使用某种容器类的数据结构,将上述的多个定时器组合起来,便于堆定时事件做统一的管理。例如使用升序的链表将所有的定时器串联组织起来。

Linux下提供了三种定时的方法:
socket选项SO_RECVTIMEO, 和 SO_SNDTIMEO
SIGALRM信号
IO复用系统调用的超时的参数
SIGALARM信号:
利用alarm函数周期的触发SIGALARM信号,信号处理函数利用管道和主循环,主循环接着收到该信号后对升序链表上所有的定时器进行处理,若该段事件内没有交换数据,则将该连接关闭,释放占用的资源。

定时器主要是处理非活动的连接模块,主要分为两个部分,一为定时方法与信号通知流程, 二定时器以及容器的设计与定时任务的处理
概括为:定时方法、信号如何通知、定时如何处理、容器的设计

API基础
sigaction结构体

struct sigaction{
void (sa_handler)(int); //函数的指针,指向信号处理函数
void (sa_sigaction)(int, siginfo_t, void
); //同样是信号处理函数,有三个参数,可以获得关于信号的更加详细的信息
sigset_t sa_mask; //用来指定在信号处理函数执行期间需要被屏蔽的信号
int sa_flags; //信号处理的行为
void (*sa_restorer)(void); //不使用
}

sa_flags:
SA_RESTART: 使得被信号打断的系统调用自动重新发起
SA_NOCLDSTOP,使得父进程在它的子进程暂停或继续运行时候不会收到SIGCHLD信号
SA_NOCLDWAIT,使得父进程在它子进程退出的时候不会收到SIGCHLD信号
SA_NODEFER,使得信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
SA_RESETHAND,信号处理之后重新设置为默认的处理方式
SA_SIGINFO,使得sa_sigaction成员而不是sa_handler作为信号处理函数

#include<signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
返回值 0表示成功 -1表示有错误发生

int sigfillset(sigset_t *set);
将所有的信号都加入到此信号集合中

#define SIGALARM 14
#define SIGTERM 15

#include <unistd.h>
unsighed int alarm(unsigned int seconds);
设置信号传送闹钟,用来设置信号SIGALARM在经过参数seconds秒后发送目前的进程。如果设置信号SIGALARM的处理函数,那么alarm()默认处理终止进程。

#include<sys/types.h>
#include <sys/socket.h>

int socketpair(int domain, int type, int protocol, int sv[2]);
domian:协议族,PF_UNIX 或是 AF_UNIX
type表示协议,可以使SOCK_STREAM SOCK_DGRAM
protocol: 0
sv[2]: 表示套接字柄对,该两个句柄作用相同,均能够进行读写双向的操作
返回结果 0成功 -1失败

send函数
#include<sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
当缓冲区满的时候,send通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区满的时候,返回EAGAIN或是EWOULDBLOCK错误,此时可以调用select函数来监视何时发送数据。

信号通知流程:
采用的是异步处理机制,信号处理函数和当前的进程是两条不同的执行的路线。当进程收到信号时候,操作系统会中断进程当前的正常流程,转而进入到信号的处理函数执行的操作,完成后在返回中断的地方继续执行。
为了避免信号竞态现象发生,信号处理期间系统不会再次的触发他。所以为了确保该信号不被屏蔽的时间太久,信号处理函数需要尽可能的执行完毕。
一般的信号处理函数需要处理该信号对应的逻辑,当该逻辑比较复杂的时候,信号的处理函数的执行的事件过长,回导致信号的屏蔽的事件太久。
信号处理函数为了避免长时间的执行所以仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在主循环中,由主循环执行信号对应的逻辑代码。
统一事件源:
将信号与其他事件一样被处理。
具体的,信号处理函数使用管道将信号传递给主循环,信号处理函数管道写端写入信号值,主循环则从管道的读端读出信号值,使用IO复用系统调用来监听读端的可读事件,这样的信号事件与其他文件描述符都可以通过epoll来监视,从而实现统一的处理

信号的处理机制:
每个进程之中,都存放着一个表,里面存着每种信号所代表的含义,内核通过内置表项中的每一位来标识对应的信号类型。

信号的接收:
接受到的任务是由内核代理的,当内核接受到信号之后,会将其放在对应进程的信号队列中,同时向进程发送一个中断,使其陷入到内核态。注意,此时信号还在队列中,对进程来说暂时是不知道有信号。
信号的检测
进程从内核态返回到用户态进行信号检测
进程在内核态中,从睡眠状态被唤醒的时候进行信号的检测
进程陷入内核态后,有两种场景对信号进行检测:当发现有新的信号的时候便会进入到下一步,信号的处理
信号的处理
(内核)信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器将其指向信号处理函数。
(用户)接下来进程返回用户态中,执行相应的信号处理函数。
(内核)信号处理函数执行完成之后,还需要返回内核态,检查是否还有其他未处理
(用户)如果所有的信号都处理完成,就会将内核栈恢复(从用户栈备份拷贝回来),同时恢复指令寄存器将其指向中断前的运行的位置,最后回到用户态继续执行进程。

定时器的设计

将连接资源、定时事件和超时时间封装为定时器类,具体的,
连接资源包括客户端套接字地址,文件描述符和定时器
定时事件为回调函数,将其封装起来由用户自定义。
定时器超时时间 = 浏览器和服务器的连接的时间+固定时间(TIMEOUT),定时器使用的时绝对的时间作为超时值。

定时器容器的设计
定时容器为双向的升序的链表,具体的为每一个连接创建定时器,将其添加到链表中,并按照超时的时间按照升序排列。执行定时任务,将到期的定时器从链表中删除。
升序链表的主要的逻辑如下:
创建头尾节点,其中头尾结点是没有意义的,仅仅统一方便调整。

如何使用定时器
服务器首先要创建定时器容器链表,然后统一事件源将异常事件,读写事件和信号处理事件统一处理,根据不同的事件的对应的逻辑使用定时器。
1.浏览器与服务器连接时候,创建该连接对应的定时器,并将该定时器添加到链表上。
2.处理异常事件时,执行定时事件,服务器关闭连接,从链表上移除对应的定时器。
3.处理定时信号时,将定时标志设置为true。
4.处理读事件时,若某连接上发生读事件,则将该连接的定时器向后移动,否则,执行定时事件。
5.处理写事件,若服务器通过某连接给浏览器发送数据,也将对应的定时器向后移动,否则执行定时事件。

统一事件源
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。但是信号处理函数要尽可能的快的执行完毕,以确保该信号不被屏蔽它(为了避免静态竞争条件,信号在处理期间,系统不会再次的触发它)。
解决方法:把信号的处理逻辑放在程序的主循环中,当信号处理函数被触发时,它只是简单的通知主循环程序接收信号,并把信号值传递给主循环,主循环在根据接收到的信号值执行目标值对应的逻辑代码。
信号处理函数如何通知主循环?
信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。
那么管道如何主循环如何知道管道的写端写入信号值,主循环则从管道的读端堵住信号值,那么主循环怎么知道管道上何时有数据可以读呢?
使用IO复用系统调用来监听读端文件描述符上的可读事件。
以上信号事件就可以和其他的事件一样被处理。
相关的信号:
SIGHUP:当挂起进程的控制终端时,SIGHUP信号将会被触发。对于没有控制终端的网络后台程序,他们会利用SIGHUP信号来轻质服务器重读配置文件。xineted超时服务器程序

SIGPIPE:默认的i情况下,往一个读端关闭的管道或是socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序收到SIGPIPE信号的默认行为时结束进程,而我们绝对不希望因为写错误而导致程序退出。引起SIGPIPE 信号的写操作将设置errno为EPIPE。

SIGURG:Linux环境下,内核通知应用程序带外数据到达主要有两种方法:一种是IO复用计数,select等系统调用在接收带外数据时将返回,并向应用程序报告socket上的异常事件,另外的一种是SIGURG信号。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值