《Linux高性能服务器编程》阅读笔记

  • bind成功时返回0,失败时返回-1并设置errno。其中,两种常见的errno是EACCES和EADDRINUSE,他们的含义分别是:
    • EACCES:被绑定的地址是受保护的地址,仅有超级用户可以访问
    • EADDRINUSE:被绑定的地址正在使用中。
  • listenbacklog参数表示:处于完全连接状态的socket的上限。
  • accept只是从监听队列中取出连接,而不管连接处于何种状态。(ESSTABLISHED或者CLOSE_WAIT
  • connect失败时返回-1并设置errno。其中,最常见的两种errno是ECONNREFUSEDETIMEDOUT
    • ECONNREFUSED:目标端口不存在,连接被拒绝。
    • ETIMEDOUT:连接超时。
  • close并非立即关闭一个连接,而是将fd的引用数减1。

    重点来了:

  • 对于监听Socket来说,在listen调用前设置这些常用的Socket选项,那么Accept返回的连接Socket将继承这些选项:SO_KEEPALIVESO_LINGERSO_RCVBUFSO_REVLOWATSO_SNDBUFSO_SNDLOWATTCP_NODELAY
  • 对于客户端Socket来说,上述的选项应该在调用Connect函数之前设置,因为Connect调用成功返回之后,TCP三次握手已经完成。
  • 下面我将详细介绍这些套接字选项:
    • SO_REUSEADDR:服务器程序可以通过设置Socket选项来强制使用陷入TIME_WAIT状态的连接占用的Socket Address
    • SO_REUSPORT:允许在一个端口上启动一个服务的多个实例,只要每个实例绑定不同的本地IP地址即可。
    • SO_RCVBUF 和 SO_SNDBUF:表示TCP接受缓冲区与发送缓冲区的大小。TCP接受缓冲区的大小最好设置为MSS(1460)的偶数倍.
    • SO_RCVLOWAT 和 SO_SNDLOWAT:表示接受缓冲区与发送缓冲区的低水平位标记。默认情况下,均为1。
    • SO_LINGER:可以通过该选项控制Close系统调用的不同的行为。默认情况下,当我们使用Close系统调用来关闭一个Socket时,Slose立即返回,TCP模块负责将该Socket发送缓冲区中的数据发送给对方。
    • SO_KEPPALIVE:发送周期性保活报文维持连接。
    • TCP_NODELAY:禁止Nagle算法,即在连接上会出现很多的小的数据块,在局域网环境下可以开启。
  • 零拷贝函数:
#include<sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
  • 零拷贝函数:
#include<fcntl.h>
ssize_t splice(int in_fd, loff_t* off_in, int out_fd, loff_t* off_out, size_t len, unsigned int flags);

//使用splice实现零拷贝反射服务器
int pipefd[2];
int ret = pipe(pipefd);
ret = splice(connectfd, NULL, pipefd[1], NULL, 65536, SPLICE_F_MORE | SPLICE_F_MOVE);
ret = splice(pipefd[0], NULL, connectfd, NULL, 65536, SPLICE_F_MORE | SPLICE_F_MOVE);
close(connectfd);
  • 针对非阻塞IO执行的系统调用总是立即返回,而不管事情是否已经发生。如果事情没有立即发生,这些系统调用就立即返回-1,就和出错返回一样,此时我们必须根据返回时设置的errno来判断具体发生了什么情况。对Accept,Send,Recv而言,事件发生未发生时errno通常被设置为EAGAIN(意味着“再来一次”)或者EWOULDBLOCK(意味着“期望阻塞”);对于Connect而言,errno则被设置为EINPEOCESS(意味着“在处理中”)。
  • IO复用函数本身是阻塞的,他们可以提高程序效率的原因在于他们具有同事监听多个IO事件的能力。
  • 服务器程序通常需要处理三类事件:IO事件定时器事件信号事件
  • 两种事件处理模式:ReactorProator
    • Reactor
      • 主线程往Epoll内核事件表中注册可读事件
      • 主线程等待可读事件发生
      • 可读事件发生时,主线程将可读事件放入请求队列
      • 主线程将睡眠在请求队列中的工作线程唤醒,处理可读事件。处理完毕之后,向Epoll注册可写事件
      • 主线程等待可写事件发生
      • 可写事件发生时,主线程将可写事件放入请求队列
      • 如此往复循环
    • Proator
      • 主线程向内核注册读完成事件,完成时,通过信号通知应用程序
      • 主线程继续处理其他逻辑
      • 主线程收到读完成事件后,将读到的数据封装成一个事件对象送入工作线程。工作线程处理完毕之后,向内核注册读完成事件,并告诉内核用户缓冲区的位置
      • 主线程继续处理其他逻辑
      • 主线程收到读完成事件,做善后处理
      • 如此往复循环
  • 三个IO复用机制
    • select:每次事件发生之后,之前注册事件都会被修改,需要用FD_ISSET进行判断,然后重新注册。
    • poll:同select,使用轮询机制
    • epollepoll对文件描述符的操作有两种模式:LT(电平触发)和ET(边沿触发)。LT模式是默认的工作模式。当设置EPOLLET时,epoll会以ET模式来操作文件描述符,ET是高效工作模式。
      • ET:当epoll_wait检测到其上有事件发生并将此次的事件通知给应用程序后,应用程序必须立刻处理该事件,因为后续的epoll_wait不会再次向应用程序通知这一事件。降低了epoll事件被触发的次数,因此效率高于LT。
      • LT:epoll_wait检测到有事件发生并将此次的事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait的时候,epoll_wait还会再次向应用通知该事件,直到该事件被处理。
      • EPOLLONESHOT:对于注册EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个事件(可读,可写或者异常),并且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
        ```

        include<sys/select.h>

        int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

FD_ZERO(fd_set* fdset);
FD_SET(int fd, fd_set* fdset);
FD_CLR(int fd,fd_set* fdset);
int FD_ISSET(int fd,fd_set* fdset);

struct timeval
{
long tv_sec; //秒数
long tv_usec; //微秒数
}

include<poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

struct pollfd
{
int fd;
short event; //注册的事件
short revent; //实际发生的事件,内核填充,记得每次判断后归零
}

POLLIN:数据可读
POLLOUT:数据可写
POLLREHUP:TCP连接被对方关闭,或者对方关闭了写操作
POLLERR:错误
POLLHUP:挂起
POLLNVAL:文件描述符没有打开

include<sys/epoll.h>

int epoll_create(int size);
int epoll_ctl(int epollfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

op参数的类型:
EPOLL_CTL_ADD:向事件注册表中注册fd事件
EPOLL_CTL_MOD:修改fd上注册的事件
EPOLL_CTL_DEL:删除fd上注册的事件

struct epoll_event
{
_uint32_t events;
epoll_data_t data;
};

union epoll_data
{
void* ptr;
int fd; //指定事件所从属的描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;

- 服务器端处理信号的方式:

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));
sa.sa_handleer = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
assrt(sigaction(sig, &sa, NULL) != -1);
}
```

  • 与网络编程相关的信号:
    • SIGHUP:挂起进程的控制终端时,SIGHUP信号会被触发。
    • SIGPIPE:往一个关闭的管道或者Socket中写入数据。默认状态是结束进程。
    • SIGURG:带外数据到达。
    • SIGCHLD:子进程状态发生变化,使用wait进行处理
    • SIGTERM:终止进程。kill命令默认发送该信号。
    • SIGINT:键盘输入以中断进程。