Linux服务器程序规范 & 高性能服务器程序框架
Linux服务器程序规范
- 一般以后台进程的形式运行,又称守护进程。父进程为init
- 通常有一套日志系统,至少能能输出到文件,大部分后台进程在/var/log目录下有自己的日志目录
- 一般使用一个专门的非root账号运行
- 通常可配置,如果选项太多,可使用配置文件
- 通常会在启动时生成一个PID文件并存入/var/run目录中,以记录该进程后台PID。比如syslogd的PID文件为 /var/run/syslogd.pid
- 要考虑系统资源和限制,以预测自身能承受多大负荷,比如进程可用文件描述符总数和内存总量等
日志
linux使用一个守护进程处理系统日志——syslogd或升级版 rsyslogd
rsyslogd接收用户进程和内核的日志。用户进程使用syslog函数生成系统日志,该函数将日志输出到unix本地域docket类型的文件/dev/log中,rsyslogd监听该文件获取用户进程输出。内核日志在老系统上通过零一个rklogd管理,rsyslogd利用额外模块实现相同功能。内核日志通过printk打印至内核的环状内存(ring buffer),其直接映射到/proc/kmsg文件中。rsyslogd通过读取该文件获得内核日志。rsyslogd会将信息输出到特定日志文件:/var/log/debug /var/log/messages /var/log/kern.log。使用 /etc/rsyslog.conf 进行配置。
syslog函数指定日志级别打印日志
openlog设置日志格式
设置日志掩码,用于过滤日志
关闭日志功能:void closelog();
资源限制
extern int getrlimit (
__rlimit_resource_t __resource, // 资源类型
struct rlimit *__rlimits) __THROW;
struct rlimit
{
/* The current (soft) limit. */
rlim_t rlim_cur; // 软限制,建议不要超过
/* The hard limit. */
rlim_t rlim_max; // 硬限制,超过可能被系统终止
};
可以使用ulimit命令修改软硬限制,只有root用户可以增加
改变工作目录和根目录
服务程序后台化
bool daemonize() {
// 创建子进程,关闭父进程,这样使程序后台运行
pid_t pid = fork();
if (pid < 0) {
return false;
} else if (pid > 0) {
exit 0;
}
// 设置文件掩码,当进程创建新文件时文件权限将为 mode & 0777
umask(0);
pid_t sid = setsid();
if (sid < 0) {
return false;
}
if ((chdir("/")) < 0) {
return false;
}
// 关闭 stdin stdout stderr
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 将标准流重定向到 /dev/null
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
return true;
}
linux提供了相同功能的库函数
extern int daemon (
int __nochdir, // 是否改变工作目录,传0时工作目录设置为 /
int __noclose // 为0时将stdio重定向到 /dev/null
) __THROW __wur;
高性能服务器程序框架
服务器程序通常要处理3类事件:io、信号、定时事件
两种高效事件处理模式 Reactor Proactor
Reactor 同步IO
主线程(IO处理单元)只负责监听fd是否有事件发生,有则通知工作线程(逻辑单元),主线程不做任何实质性工作。读写数据、接受新连接、处理客户请求都在工作线程内完成。
使用同步IO模型epoll实现:
- 主线程网epoll内核事件表种注册socket上读就绪事件
- 主线程调用epoll_wait 等等待 socket 上有数据可读
- 当 socket 上有数据可读,epoll_wait 通知主线程。主线程将 socket 可读事件放入请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后向epoll事件表种注册该 socket 上的写事件
- 主线程调用 epoll_wait 等待 socket 可写
- 当socket可写,epoll_wait 通知主线程。主线程将socket可写事件放入请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果
Proactor 异步IO
所有io操作都交给主线程和内核处理,工作线程仅负责业务逻辑。
使用异步IO模型(以 aio_read 和 aio_write 为例):
- 主线程调用 aio_read 向内核注册 socket 的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序,比如信号
- 主线程继续处理其他逻辑
- 当socket上数据被读入用户缓冲区后,内核将向应用程序发送一个信号,通知应用程序可用
- 应用程序预定义好的信号处理函数选择一个工作线程处理客户请求。工作线程处理完客户请求后,调用aio_write向内核注册socket写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序以信号为例
- 主线程继续处理其他逻辑
- 当用户缓冲区数据被写入socket后,内核向应用发送一个信号,通知数据发送完毕
- 应用程序预先定义好的信号处理函数选择一个工作线程来善后处理,比如是否关闭socket
同步IO模拟Proactor模式
主线程执行数据读写操作,读写完成后,主线程向工作线程通知这一“完成事件”。那么从工作线程角度来看,他们直接获得数据读写操作结果,接下来只要对结果进行逻辑处理
- 向epoll内核注册socket读就绪事件
- 调用 epoll_wait 等待 socket 上有数据可读
- 当socket上有数据可读,epoll_wait通知主线程。主线程从socket循环读取数据,知道读完,然后将数据封装成一个对象出入请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,获得请求对象并处理客户请求,然后向epoll内核事件表种注册socket上写就绪事件
- 主线程调用 epoll_wait 等待 socket 可写
- 当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求结果
并发模式
用于IO密集型程序,指IO处理单元和多个逻辑单元间协调完成任务的方法。服务器主要有两种并发编程模式:
1. 半同步/半异步模式
在io模型种“同步”“异步”区分的是内核向程序通知的是何种io事件(就绪事件还是完成事件),以及谁来完成io读写(应用还是内核)。并发模式中,“同步”指程序完全按照代码序列顺序执行,“异步”指程序的执行需要由系统事件驱动,如终端、信号等事件。
半同步半异步模型(half-sync/half-async)中,同步线程用于处理客户逻辑,异步线程用于处理io事件。异步线程监听到客户请求后将其封装成请求对象并插入请求队列。请求队列将通知某个工作在同步模式的工作线程读取并处理该请求对象。具体选哪个工作线程取决于请求队列设计。
半同步半反应堆模式(half-sync/half-reactive)是这种模式的一种变体,异步线程只有一个由主线程充当。它监听所有socket上事件。由可读事件时主线程得到新的连接socket并向epoll内核事件表中注册该socket读写事件;若连接socket上由读写事件主线程就将该连接socket插入请求队列。所有工作线程都睡眠在请求队列上,当有任务时,他们通过竞争(不如互斥锁)获得任务。
主线程将就绪的连接socket插入请求队列,这说明半同步/半反应堆模式采用Reactor模式:它要求工作线程自己从socket上读取客户请求和向socket写入应答,这就是“half-reactive”的含义。
缺点:
- 请求队列被共享。主线程和工作线程操作请求队列时都要加锁保护。
- 每个工作线程在同一时间只能处理一个客户请求。若客户数量多工作线程少,请求队列中将堆积多个任务对象,相应速度越来越慢。若通过增加工作线程,线程的切换也耗费大量cpu
在这个“高效的半同步/半异步模式”中,主线程只监听socket,连接socket由工作线程管理。当由新连接来时,主线程就接受并发给某个工作线程,此后该socket的所有io操作都有这个工作线程处理,知道关闭连接。派发socket最简单的方式就是向之间的管道写数据。工作线程从管道接收到新连接后把新socket的读写事件注册到自己的epoll内核事件表中。
主线程和所有工作线程都维持自己的事件循环,各自监听不同事件。所以这种模式中每个线程都工作在异步模式,并非严格的半同步/半异步模式。
2. 领导者/追随者模式
多个工作线程轮流获得事件源集合,轮流监听、分发处理事件的一种模式。任意时间点程序都只有一个leader,它监听io事件;其他为follower,休眠在线程池中等待成为新领导者。当leader检测到io事件,要先从线程池中选出新leader,然后处理io事件。新leader等待新io事件,原leader则处理io事件,二者并发。
包含组件:
- 句柄集 HandleSet
- 线程集 ThreadSet
线程有3种状态 Leader Processing Follower
leader推选新线程成领导者、追随者等待成为新领导者都会修改线程集,因此线程集提供一个成员 Synchronizert 同步这两个操作以避免竞态条件 - 事件处理器 EventHandler
事件处理器包含一个或多个回调函数 handle_event,用于处理事件对应的业务逻辑。使用前要绑定到某个句柄上,当该句柄有事件时leader执行与之绑定的事件处理器中的回调函数。 - 具体的事件处理器 ConcreteEventHandler
必须重写基类中的 handle_event 方法,以处理特定任务
领导者追随者模式中leader线程监听io事件并处理请求,因而无需传递任何数据或操作同步队列。但一个缺点是:仅支持一个事件源集合,因此无法让每个工作线程独立地管理多个客户连接。
提高服务器性能的方法
- 池
- 避免数据复制
尽可能使用零拷贝,如 sendfile 共享内存 - 避免上下文切换
线程切换会消耗大量cpu资源 - 锁