Linux高性能服务器编程
要补充的基础知识太多了,大黑书真的很难啃,先看这本书吧,然后再继续—游双
第一篇 TCP/IP详解
第1章 TCP/IP协议族
- 协议族分为4层,应用层(ping, telnet, OSPF, DNS),传输层(TCP, UDP),网络层(ICMP, IP),数据链路层(ARP, RARP)
- 数据链路层ARP和RARP地址解析协议和逆地址解析协议,实现了IP地址和物理地址之间的转换。
- 网络层实现数据包的选路和转发。IP协议通过数据包的目的IP决定如何投递。ICMP主要用于网络检测,分为差错报文和查询报文。ICMP不是严格意义上的网络层协议,因为它使用IP协议提供的服务。
- 传输层提供端到端的通讯。
- TCP协议为应用层提供可靠、面向连接和基于流的服务。
- UDP协议为应用层提供不可靠、无连接和基于数据报的服务。
- 应用层负责处理应用程序的逻辑。应用层是在用户态实现的,其他层是在内核态实现的。
- 经过TCP封装后的数据称为TCP报文段,或简称TCP段。经过UDP封装后的数据称为UDP数据段。经过IP封装后的数据称为IP数据段。经过数据链路层封装的数据称为帧。
- 当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层依次处理帧中本层负责的头部信息,以获取所需信息,并最终将处理后的帧交给目标应用程序。这个过程叫分用。
- ARP的工作原理是:主机向自己所在的网络广播一个ARP请求,请求中包含目标机器的网络地址,该网络上所有主机都会收到请求,但是只有被请求的目标机器会得到回答,其中包含自己的物理地址。
- 操作系统需要实现一组系统调用,使得应用程序能够访问在内核中实现的协议提供的服务,这一套系统调用就是socket。它是一套通用的网络编程接口,除了访问内核中的TCP/IP协议栈,而且可以访问其他的协议栈。
第二章 IP协议详解
- IP协议提供无状态、无连接、不可靠的服务
- 无状态是指通信双方不同步传输数据的状态信息。无法处理乱序和重复的IP数据报。优点是简单高效。
- 无连接是指的IP通信双方都不长久的维持对方的任何信息。
- 不可靠指的是不能保证IP数据报准确的到达接受端。
- 路由表进行更新的时候使用边际网关协议BGP或者路由信息协议RIP进行更新。
- IPV6不是IPV4的简单扩展,而是完全独立的协议,用以太网帧封装的IPV6和IPV4具有不同的类型值。IPV4为0x800,IPV6为0x86dd。
第三章 TCP协议详解
- TCP协议是一对一的,基于广播和多播的应用程序应该使用UDP。
- 字节流服务和数据报服务体现在实际编程中就是通信双方是否执行相同次数的读和写操作。TCP是基于字节流的,也就是说发送的报文段和写次数之间没有必然联系。
- TCP建立连接使用三次握手,第三次握手的时候就可以携带信息。断开连接的时候使用的四次挥手,第二次挥手的确认断开连接是可以省略的。
- TCP连接是全双工的,所以允许两个方向的数据被独立的关闭,但是使用的很少。
- TCP超时进行重传的时候是按照1s->2s->4s……这样的顺序进行的,具体重传的次数,写在了/proc/sys/net/ipv4/tcp_syn_retries文件中
- 复位报文段,以通知对方关闭连接或者重新建立连接,一般可以分为,访问不存在的端口,异常终止连接,处理半打开连接。
- TCP报文段携带的应用程序数据,按照长度分为,交互数据和成块数据。
- 有些传输层具有带外数据(out of Band, OOB)的概念,用于迅速通告对方本端发生的重要事件。带外数据具有更高的优先级。很少见,telnet和ftp等远程非活跃程序。
- UDP是没有带外数据的,TCP是没有真正的带外数据的,TCP使用的是紧急数据和紧急指针。
- TCP的超时重传就是在规定时间内没有收到回复,就是进行重传。
- TCP的拥塞控制包括,慢启动、拥塞避免、快速重传和快速恢复。在Linux上有reno算法,vegas算法,cubic算法等,在/proc/sys/net/ipv4/tcp_congestion_control中指示了当前的拥塞算法。
第四章 访问案例
- HTTP的代理服务器有时可以提供缓存目标资源的功能,可以按照方法和作用分为
- 正向代理服务器,需要客户端自己设置代理服务器
- 反向代理服务器,设置在服务端,是用来转发到服务端内部网络的服务器,对外表现为一个真实的服务器
- 反向代理服务器设置在网关上,可以看成是正向代理的一种特殊情况。
- 代理服务器访问DNS服务器查询域名对应的IP,根据IP查询路由器MAC地址,进行HTTP通信。
- cookie是服务器发送给客户端的特殊信息。客户端每一次像服务器请求服务的时候就需要带上这些信息,服务器也就区分了不同的客户。
第二篇 深入理解高性能服务器编程
第五章 Linux网络编程基础API
现代PC大多采用小端字节序,因此小端字节序列又称为主机字节序。
而发送的时候,发送端需要将小端字节序列转换为大端字节序,所以大端字节序列也叫做网络字节序列。同一台主机上面的两个进程进行通信的时候也是需要考虑字节序列的问题,比如,java一般使用大端字节序。
可以使用以下的三个函数来将IP地址进行转换,还有一些函数,使用时查阅资料即可
in_addr_t inet_addr(const char * strptr);
int inet aton(const char* cp, struct in_addr*inp);
char* inet _ntoa(struct in_addr in);
socket本质上就是一个可读,可写,可控制,可关闭的文件描述符。
创建socket 使用
int socket(int domain, int type, int protocal);
命名socket将一个socket与socket地址绑定称为给socket命名。在服务器程序中,通常需要命名socket,而在客户端通常不需要。绑定使用的函数是
int bind(int sockfd, const struct* my_addr, socklen_t addrlen);
监听socket使用的是
int listen(int sockfd, int backlog);
第二个参数是内核监听队列的最大长度,典型值为5。接收连接使用的是
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
用于在服务器端接受客户端的连接请求,并创建一个新的套接字来处理与客户端的通信- 参数:
int sockfd
:服务器监听套接字,即调用socket
、bind
和listen
后返回的套接字描述符。struct sockaddr *addr
:指向保存客户端地址信息的结构体指针,用于接收客户端的地址信息。socklen_t *addrlen
:指向addr
结构体长度的指针,调用后返回客户端地址结构体的实际长度。
- 返回值:
- 返回一个新的套接字描述符,用于与客户端进行通信。该套接字描述符可以使用
read
和write
函数进行数据传输。 - 如果出错,返回值为 -1。
- 返回一个新的套接字描述符,用于与客户端进行通信。该套接字描述符可以使用
- 参数:
客户端发起连接使用的是
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
- 参数:
int sockfd
:客户端套接字,即调用socket
后返回的套接字描述符。const struct sockaddr *addr
:指向服务器地址信息的结构体指针,用于指定连接的服务器地址。socklen_t addrlen
:服务器地址结构体的长度。
- 返回值:
- 如果连接成功,返回值为 0。
- 如果出错,返回值为 -1。
- 参数:
关闭连接也就是关闭连接所对应的socket。可以使用
close(fd)
,这样会在fd的引用数减1,减为0时才正式关闭。向立即关闭,可以使用int shutdown(int sockfd, int howto)
,howto可以使用SHUT_RD, SHUT_WR, SHUT_RDWR。TCP数据读写可以使用write和read,当然,也可以适用
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
UDP数据读写,使用下面的系统调用。当然下面的两个函数也可以用于面向连接socket的数据读写,只要把最的两个参数这只为NULL即可。
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
通用数据读写函数
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
得到一个连接socket的本端socket地址和远端socket地址
int getsockname(int sockfd, struct* address, socklen_t* address_len);
获得sockfd对应的本端socket地址int getpeername(int sockfd, struct* address, socklen_t* address_len);
获得sockfd对应的远端socket地址
fcntl系统调用时控制文件描述符属性的通用的POSIX方法,下面两个函数是专门用来读取和设置sock文件描述符属性的方法
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);
SO_REUSEADDR
是一种套接字选项,用于设置套接字的地址重用属性。当设置了SO_REUSEADDR
选项后,即使套接字被关闭,仍然可以立即重新绑定相同的地址和端口。这在服务器程序需要快速重启并绑定相同地址和端口时非常有用。SO_RCVBUF
和SO_SNDBUF
是套接字选项(Socket Option)的两种,用于设置套接字接收缓冲区和发送缓冲区的大小。它们分别控制了在 TCP 协议中接收数据和发送数据的缓冲区大小。一般情况下,当我们设置缓冲区大小的时候,系统会将其加倍,并且不能小于最小值。接收缓冲区最小为256字节,发送缓冲区最小为2048字节。SO_RCVLOWAT
和SO_SNDLOWAT
是套接字选项(Socket Option)的两种,用于设置套接字的接收低水位标志和发送低水位标志。它们分别控制了在 TCP 协议中接收数据和发送数据的低水位标志。一般是用来IO复用的时候判断socket是否可读或可写。SO_LINGER
是套接字选项(Socket Option)之一,用于设置套接字关闭时的行为。当我们使用close来关闭一个socket的时候,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发给对方。网络信息API
gethostbyname
和gethostbyaddr
根据主机名获取主机的完整信息,根据IP地址获取主机的完整信息,都是通过读取/etc/hosts进行的getservbyname
和getservbyport
根据名称获取某个服务的完整信息,根据端口号获取某个服务的完整信息,都是通过读取/etc/services实现的getaddrinfo
既能通过主机名获取IP地址,也能通过服务名获得端口号getnameinfo
函数通过socket地址可以同时获得以字符串表示的主机名和服务名
第六章 高级IO函数
pipe函数是用来创建一个管道,管道内的数据是字节流,这和TCP字节流的概念相同,但是也有细微得差别。往TCP中写多少字节取决于接受通知窗口大小和本端拥塞窗口大小。
Linux中有一个基础API
int sockpair(int domain, int type, int protocal, int fd[2])
可以方便的创建双向管道。只能使用本地协议族。dup函数和dup2函数可以用来复制文件描述符,也经常用于重定向标准输入、输出和错误,或者在使用管道时复制文件描述符。
int dup(int oldfd);
int dup2(int oldfd, int newfd);
readv函数和writev函数,readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数将多块分散的内存块一并写入文件描述符中,即集中写。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
sendfile函数在两个文件描述符之间直接进行数据传递(完全在内核中进行操作),避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这也被称为零拷贝。in_fd 必须指向待读出的文件描述符,必须是一个真实的文件,out_fd必须是一个socket
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
mmap函数和munmap函数前面的用于申请一段内存,这段内存可以作为进程间通信的共享内存,也可以将文件直接映射到其中,munmap函数用来释放该内存。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- prot参数指定了映射区的保护方式,常见的值有 PROT_READ、PROT_WRITE和PROT_EXEC
int munmap(void *addr, size_t length);
**splice函数 **用于在两个文件描述符中移动数据,也是零拷贝操作。
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
tee函数在两个管道文件描述符之间复制数据,也是零拷贝。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
注意**
sendfile
主要用于文件和套接字之间的数据传输,而tee
则主要用于在两个文件描述符之间复制数据。**fcntl函数提供了对文件描述符各种控制操作,
ioctl函数
比它的执行的控制会更多,但是fcntl函数
是POSIX规范指定的首选方法int fcntl(int fd, int cmd, ...);
第七章 Linux服务器程序规范
- 除了网络通信外,服务器程序还要考虑很多其他细节,这些细节广且零碎,单都是模板式的,我们一般称之为服务器程序规范。
- 服务程序以后台运行的方式进行,后台进程又称守护进程
- 在/var/log拥有日志记录
- 以某一个非root身份运行
- Linux程序一般是可配置的
- Linux进程会在启动的时候生成一个pid文件
- Linux服务器程序通常需要考虑系统资源和限制
- Linux一般提供一个守护进程来处理系统日志—-syslogd,不过现在使用的是它的升级版本rsyslogd。这个紧张既能几首用户进程输出的日志,又能接收内核日志。
- syslog函数
void syslog(int priority, const char *format, ...);
LOG_EMERG
:紧急情况,系统不可用。LOG_ALERT
:需要立即采取行动。LOG_CRIT
:临界条件,系统出现严重错误。LOG_ERR
:一般错误。LOG_WARNING
:警告信息。LOG_NOTICE
:一般重要的系统消息。LOG_INFO
:信息性消息。LOG_DEBUG
:调试信息。
- 日志的过滤。一般来讲程序开发的时候,需要很多的调试信息,发布之后不用删除代码,使用设置日志掩码即可。
int setlogmask(int mask);
可以使用如下的函数关闭日志功能void closelog();
- 大多数服务器必须以root身份启动,但是不能以root身份运行。
- UID 真实用户id
- EUID 有效用户id
- GID 真实组id
- PGID 有效组id
- 一个进程一般拥有两个id,UID和EGID。有效用户为root的进程称为特权进程。如果一个程序设置了set-user-id标志,那么任何普通用户运行这个程序的时候,有效用户就是这个程序的所有者。而不是运行这个程序的用户。它允许一个普通的用户以程序所有者的权限来执行该程序。
- 一个进程都隶属于一个进程组,除了PID之外,还有进程组ID(PGID),可以使用如下的函数来进行获取指定进程的PGID
pid_t getpgid(pid_t pid);
。每一个进程组都有一个首领进程,它的PGID和PID相同。 - 可以使用
int setpgid(pid_t pid, pid_t pgid)
来设置某一个进程的pgid。 - 一些有关联的进程组将形成一个会话,可以使用
pid_t setsid(void);
来创建会话。这个函数不能由进程组的首领进程调用,否则将产生错误。非首领进程创建会话的时候,还会有如下的效果- 调用进程成为会话的首领,此时该进程是新会话的唯一成员
- 新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领
- 调用进程将甩开终端。
- 会话ID(SID)其实就可以理解为会话首领所在的进程组的PGID,可以使用
pid_t getsid(pid_t pid);
来读取SID。 - 使用ps命令可以查看进程、进程组和会话之间的关系
ps -o pid, ppid,pgid, sid,comm | less
- 系统资源可以通过如下的函数来进行读取和限制,我们可以使用ulimit来进行限制
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
- Web服务器的逻辑根目录并非文件系统的根目录“/”,而是站点的根目录,一般是/var/www/。获取当前工作目录和改变进程目录使用
char* getcwd(char* buf, size_t size);
int chdir(const char* path);
- 使用如下函数来改变进程根目录
int chroot(const char* path);
- 编写守护进程,Linux提供了同样功能的库函数
int daemon(int nochdir, int noclose);
第八章 高性能服务器框架
服务器模型 C/S模型。服务器在处理一个客户请求的时候,还会继续监听其他的客户请求。适合资源相对集中的场合,访问量比较大得时候,所有的客户得到很慢的相应。
服务器P2P模型。每台机器消耗服务的同时,也给别人提供服务。当用户之间请求过多的时候,网络负载将加重。另外,主机之间很难互相发现,实际使用的时候一般有一个发现服务器,提供查找之类的服务。
服务器的基本框架IO处理单元,请求队列,逻辑单元,请求队列,网络存储单元(可选)。
服务器的功能描述
模块 单个服务器程序 服务器集群 IO处理单元 处理客户连接,读写网络数据 作为接入服务器,实现负载均衡 逻辑单元 业务进程或线程 逻辑服务器 网络存储单元 本地数据库、文件或缓存 数据库服务器 请求队列 各单元之间的通信方式 各服务器之间的永久的TCP连接 阻塞和非阻塞的概念能够应用于所有文件描述符,而不仅仅是socket。所以我们只有在时间已经发生的情况下,操作非阻塞IO,才能提高程序的效率,所以一般非阻塞IO和其他IO通知机制一起使用,比如IO复用,SIGIO信号。
- 针对阻塞IO执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的时间发生为止。在socket基础API中,可能被阻塞的系统调用有accept、send、recv、connect
- 针对非阻塞IO执行系统调用总是立即返回。不管事件是否已经发生,如果事件没有发生就返回-1.
一般情况下,同步IO向应用程序通知的是IO就绪事件,而异步IO通知的是IO完成事件
IO模型比较
IO模型 读写操作和阻塞阶段 阻塞IO 程序阻塞于读写阶段 IO复用 程序阻塞于IO复用系统调用,但是可以同时监听多个IO事件。对于IO本身的读写是非阻塞的 SIGIO信号 信号触发读写就绪事件,用户程序执行读写操作。程序没有阻塞阶段 异步IO 内核执行读写操作并触发读写完成事件。程序没有阻塞阶段 两种高效的时间处理模式,Reactor和proactor
- Reactor模式,主线程(IO处理单元)只负责监听文件描述符上是否有时间发生,有的话立即将该事件通知工作线程(逻辑单元)
- Proactor模式,将所有的IO操作交给主线程和内核来处理,工作线程仅仅负责业务逻辑
如果程序是计算密集型的,并发编程没有优势,如果是IO密集型的,将很有优势。
两种高效的并发模式,半同步/半异步模式和领导者/追随者模式
- 半同步/半异步模式
- 这里的同步异步和上面是不一样的概念,这里同步指的是,程序完全按照代码序列的顺序执行,异步指的是程序的执行需要由系统时间驱动。系统事件包括中断,信号等。
- 异步执行效率高,实时性强,但是编写困难,不适合于大量的并发。同步线程效率较低,实时性差,但是逻辑简单。所以使用半同步/半异步模式
- 半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理IO事件
- 有一种变种半同步/半反应堆模式,它有如下的缺点,主线程和工作线程共享请求队列,每个工作线程在同一个时间只能处理一个客户请求。
- 领导者/追溯者模式,多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种方式
- 在任意的时间点,程序都仅有一个领导者线程,负责监听IO事件,其他的线程都是追随者,休眠在线程池,当领导者检测到IO事件,就推举出新的领导者,然后处理IO事件,实现了并发。
- 包含以下几个组件,句柄集、线程集、时间处理器、具体的事件处理器。
- 句柄集:表示Io资源,在LInux下通常是一个文件描述符
- 线程集:所有工作线程的管理者,线程集中的任意线程处于以下三种状态,Leader,Processing,Follower
- 事件处理器和具体时间处理器,包含一个或多个处理函数。
- 半同步/半异步模式
有限状态机是一种高效的编程方法
提高服务器性能的其他建议—-从软环境提升服务器性能
- 池,用空间换取时间,这就是池的概念。池是一组资源的集合,在服务器启动之初就完全创建好,并初始化,这称为静态资源分配。常见的包括内存池(用于socket的接收缓存和发送缓存),进程池、线程池和连接池(用于服务器或服务器机群的内部永久链接)
- 数据复制,应该避免不必要的数据复制,尤其是数据复制发生在用户代码和内核之间的时候。可以考虑使用“零拷贝”。
- 上下文切换,进程切换或线程导致的系统开销。当线程的数量不大于CPU的数目的时候,上下文切换就不是问题了。
- 锁,通常被认为是导致服务器效率低下的一个因素。
第九章 IO复用
IO复用能同时监听多个文件描述符,下面是使用IO复用的情况,注意IO复用本身是阻塞的
- 客户端要同时处理多个socket。如非阻塞connect技术
- 客户端要同时处理用户输入和网络连接。比如聊天室程序
- TCP服务器要同时监听socket和连接socket。这是应用最多的场景
- 服务器要同时处理TCP请求和UDP请求。比如回射服务器
- 服务器要同时监听多个端口,或者多种服务。比如xineted服务器
select系统调用,在一段指定的事件内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。成功时返回就绪(可读、可写和异常)文件描述符的总数。
下面情况可以认为socket是可读的
- socket内核接收缓冲区中的字节数大于等于其低水平标记SO_RCVLOWAT。此时可以无阻塞的读该socket,并且读操作返回的字节数大于0。
- socket通信的对方关闭连接。此时该socket的读操作将返回0
- 监听socket上有新的连接请求
- socket上有未处理的错误,此时我们可以使用getsockopt来读取和清除该错误。
下面情况是可写的
- socket内核发送缓冲区的可用字节数大于或等于其低位水平标记SO_SNDLOWAT。此时可以无阻塞的写该socket,并且写返回的字节数大于0。
- socket的写操作被关闭,对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号
- socket使用非阻塞connet连接成功或者失败(超时)之后
- socket上有未处理的错误,此时我们可以使用getsockopt来读取和清除该错误。
socket上接收带外数据将处于异常状态
poll系统调用在指定的时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。使用上与select类似
epoll使用一组函数来完成任务,而且把用户关心的文件描述符上的事件放在内核里的一个事件表中,这个表叫内核事件表
epoll_wait函数是epoll系列系统调用的主要接口,它在一段超时时间内等待一组文件描述符上的事情。如果检测到时间就把就绪事件从内核事件表中复制到它的第二个参数里面。
epoll对文件描述符的操作由两种模式:LT(Level Trigger, 电平触发)模式和ET(Edge Trigger, 边沿触发)模式,LT是默认的工作方式。
- 在 LT 模式下,epoll_wait 函数会返回所有就绪的文件描述符,直到这些文件描述符上的事件被处理掉。
- 即使文件描述符上的事件还没有被完全处理,下一次调用 epoll_wait 时,仍然会再次返回该文件描述符。
- LT 模式是 epoll 的默认工作模式。
- 在 ET 模式下,epoll_wait 函数只会返回处于变化状态的文件描述符,即只返回文件描述符上发生变化的事件。
- 一旦文件描述符上的事件被处理掉,下一次调用 epoll_wait 时不会再返回该文件描述符,除非该文件描述符上的事件发生了新的变化。
- ET 模式需要程序确保在处理文件描述符上的事件时要处理完整,否则可能会导致事件丢失。
即使是使用了ET模式,一个socket上的某个事件也有可能被触发多次,在并发中会引起问题。我们可以通过注册EPOLLONESHOT来进行实现。
select、poll和epoll的区别
系统调用 select poll epoll 事件集合 用户通过3个参数分别传入感兴趣的可读、可写及异常事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这三个参数 统一处理所有事件类型,因此只需要一个事件集参数,用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件 内核通过一个事件表直接管理用户感兴趣的所有事件,因此每次调用epoll_wait时,无须反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪事件 应用程序索引就绪文件描述符所用的事件 O(n) O(n) O(1) 最大支持文件描述符数 一般有最大限制 65535 65535 工作模式 LT LT 支持ET高效模式 内核实现与工作效率 采用轮询的方式来检测就绪事件,时间复杂度O(n) 采用轮询的方式来检测就绪事件,时间复杂度O(n) 采用回调的方式来检测就绪事件,时间复杂度O(1) IO复用有以下几个高级应用
- 非阻塞connect
- 聊天室程序
- 同时处理TCP和UDP服务
xinetd 是一种用于管理网络服务的守护进程,它负责监听指定的网络端口,并根据配置文件中的设置来启动、停止或重启相应的服务程序。
第十章 信号
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或异常,信号可以由以下方式产生
- 对于前台进程,用户卡伊使用特殊终端字符来给他发送信号。比如Ctrl+C发送中断信号
- 系统异常
- 系统状态变化
- 运行或调用kill函数
一个进程给其他进程发送信号的API是kill函数
int kill(pid_t pid, int sig);
kill函数的pid参数及意义
pid参数 含义 pid > 0 信号发给PID为pid的进程 pid = 0 信号发给本进程组内的其他进程 pid = -1 信号发给除了init进程外的所有进程,但是发送者需要拥有对目标进程发送信号的权限 pid<-1 信号发给组ID为-pid的进程组中的所有成员 kill函数出错的情况
errno 含义 EINVAL 无效的信号 EPERM 该进程没有权限发送信号给任何一个目标进程 ESRCH 目标进程或进场组不存在 信号处理函数应该是可重入的,否则容易引起一些竞态条件。
可重入(reentrant)是指一个函数在被多个任务(线程或进程)同时调用时,能够正确地处理多个并发调用,而不会出现不确定的行为或数据损坏的情况。
- 无共享数据:可重入函数不会使用全局变量或静态变量等共享数据,而是将所有数据都作为参数传递给函数或者在栈上分配,从而避免了数据的共享和竞争条件。
- 无静态变量:可重入函数不使用静态变量,因为静态变量会在不同的调用之间保留状态,如果多个调用同时使用静态变量,就会出现数据竞争的情况。
- 不调用不可重入函数:可重入函数不会调用其他不可重入函数,因为不可重入函数可能会修改全局状态或共享数据,从而影响到其他并发调用。
- 不依赖全局状态:可重入函数不依赖全局状态或者其他调用的执行状态,它的行为仅由输入参数决定,因此不会受到其他调用的影响。
Linux信号,都定义在bits/signum.h头文件中。
可以使用signal函数来进行信号设置处理函数
_sighandler_t signal(int sig, _sighandler_t _handler);
返回一个函数指针。也可以使用sigaction函数这个更健壮的接口来处理信号,
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);
Linux使用数据结构sigset_t来表示一组信号,也就是信号集函数
sigprocmask
是一个用于设置和获取进程信号屏蔽字(signal mask)的系统调用。int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
信号掩码(signal mask)是一个位向量,用于指定在某个时间点上哪些信号被屏蔽。每个进程都有一个信号掩码。当某个信号的对应位在信号掩码中被设置为 1 时,表示该信号被阻塞,进程在接收到这个信号时不会立即处理,而是将其挂起等待。当信号掩码中对应位为 0 时,表示该信号未被阻塞,进程会正常处理接收到的信号。
将信号事件和其他的IO事件一样被处理,这就是统一事件源。
网络编程相关的信号SIGHUP,当挂起进程的控制终端时,SIGHUP信号将被触发。
SIGPIPE信号,默认情况下,往一个读端关闭或socket连接中写数据将引发SIGPIPE信号。
SIGURG信号表示接收到带外数据。
第十一章 定时器
两种高效的定时器容器:时间轮和时间堆
定时是指在一段时间之后触发某段代码的机制。Linux中提供了三种定时方法
- socket选项SO_RCVTIMEO和SO_SNDTIMEO
- SIGALRM信号
- IO复用系统调用的超时参数
定时器容器:是容器类数据结构,比如时间轮;定时器:容器内容纳的一个个对象,是对定时事件的封装。
一般而言,SIGALRM信号按照固定的频率生成,即由alarm或setitimer函数设置的定时周期T不变。如果某一个定时任务的超时事件不是T的整数倍,那么实际事件和预期时间将会有偏差。
时间轮(Time Wheel)是一种用于管理定时器的数据结构,它通常用于实现高效的定时任务调度。利用哈希的思想。
时间堆(Time Heap)是一种数据结构,通常用于实现定时器管理和定时任务调度。与时间轮相比,时间堆更适用于动态添加和删除定时任务的场景,而不需要事先确定时间片段的数量。
时间轮和时间堆时间复杂度比较
类别 添加定时器 删除定时器 执行定时器 时间轮 O(1) O(1) O(n),实际好很多,接近于O(1) 时间堆 O(lgn) O(1) O(1)
第十二章 高性能IO框架库Libevent
- IO事件、信号和定时事件。我们应该考虑下面的三类问题
- 统一事件源。利用IO复用系统调用来管理所有事件
- 可移植性。不同的操作系统有不同的IO复用方式,Solaris的dev/poll文件,FreeBSD的kqueue机制,Linux下的epoll系列系统调用
- 对于并发的支持
- 有很多的IO框架很出色,ACE、ASIO和Libevent等。IO框架库以库函数的形式,封装了较为底层的系统调用。提供了一组更便于使用的接口。
- 各种IO框架库的实现原理基本相似,要么以Reactor模式实现,要么以Proactor模式实现。基于Reactor方式实现的IO框架一般包含
- 句柄,IO框架要处理的对象,即IO事件。信号和定时事件,统一称为事件源。在Linux中,IO事件对应的句柄就是文件描述符,信号事件对应的句柄就是信号值。
- 事件多路分发器,IO框架一般将系统支持的各种IO复用系统调用封装成统一的接口,称为事件多路分发器。
- 事件处理器和具体事件处理器,事件处理器执行事件对应的业务逻辑。
- Libevent是高性能的IO框架库,使用它的例子有高性能的分布式对象缓存软件memcached,google浏览器的Linux版本(早期版本)。该IO框架具有以下特点
- 跨平台支持
- 统一事件源
- 线程安全
- 基于Reactor模式实现
第十三章 多进程编程
- fork的时候子进程会复制父进程的数据,但是采用的是写时复制。即使这样,我们的程序中分配了大量的内存的时候,使用fork也应该小心。
- 当我们需要在子进程中执行其他程序的时候,即替换掉当前的进程影像,就需要使用exec系列函数之一。exec函数一般是不返回的
- 处理僵尸进程可以使用
pid_t wait(int* stat_loc);
这里的wait是阻塞的,可以使用pid_t waitpid(pid_t pid, int* stat_loc, int options);
这个非阻塞的来进行 - 管道只能用于 有关联的两个进程间的通信(比如父子进程)。FIFO管道,也叫命名管道,也能实现无关进程之间的通信。
- Linux的有关信号量的API有三个
- semget系统调用
- semop系统调用
- semctl系统调用
- 共享内存是最高效的IPC机制,因为不涉及进程之间的任何数据的传输,有关的API有4个
- shmget系统调用
- shmat系统调用
- shmdt系统调用
- shmctl系统调用
- 消息队列是在两个进程之间传递二进制数据的一种简单有效的方式。有关的API
- msgget系统调用
- msgsnd系统调用
- msgrcv系统调用
- msgctl系统调用
- 传递一个文件描述符,并不是传递文件描述符的值,而是要在接收进程中创建一个文件描述符,并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的页表项。父进程可以很轻松的将文件描述符传递给子进程,两个不相干的进程之间进行传递的时候利用UNIX域socket在进程间传递特殊的辅助数据。
第十四章 多线程编程
- 现在的线程库称为NPTL,更符合POSIX标准,已经成为glibc的一部分。采用了1:1的方式实现,即一个用户空间线程被映射为一个内核线程。
- 线程分为内核级线程和用户级线程。线程的实现方式可以分为三种:完全在用户空间实现、完全由内核调度和双层调度。
- LinuxThreads线程库的内核线程是用clone系统调用创建的进程模拟的。但是这种用进程来模拟内核线程会导致很多语义问题。它中间出现了管理线程的,增加了额外的系统开销。
- 线程创建,退出,回收,取消
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void** retval);
int pthread_cancel(pthread_t thread);
pthread_attr_t
结构体定义了一套完整的线程属性。- 线程也需要考虑同步问题,一般使用3种机制,POSIX信号量、互斥量和条件变量
- Linux上面的信号量由两组,一组是System V IPC信号量,一组是POSIX信号量
sem_init
(),sem_destroy()
,sem_wait()
,sem_trywait();
,sem_post()
- POSIX互斥锁基础API有下面5个函数
pthread_mutex_init
(),pthread_mutex_destroy()
,pthread_mutex_lock()
pthread_mutex_trylock()
pthread_mutex_unlock()
pthread_mutexattr_t
结构体定义了一套完整的互斥锁属性- 条件变量的相关函数
pthread_cond_init
pthread_cond_destroy
pthread_cond_broadcast
pthread_cond_signal
pthread_cond_wait
- 如果一个函数能够被同时调用且不发生竞态条件,我们称其为线程安全的,或者说是可重入的函数。Linux库函数只有一小部分是不可重入的。为什么不能重入是因为内部使用了静态变量。Linux提供了可重入版本,需要在原函数尾部加上_r。
- 每一个线程都可以独立的设置信号掩码
第十五章 进程池和线程池
- 动态创建子进程来实现并发服务器,有如下的缺点
- 动态创建比较耗时,导致较慢的客服响应
- 创建的子进程通常只为一个客户服务。这会导致产生大量的细微进程,进程间的切换浪费大量时间
- 动态创建的子进程是当前进程的完整映像。使得系统可用资源急剧下降
- 进程池中所有子进程都运行着相同的代码,具有相同的属性,比如优先级,PGID等。这些子进程相对“干净”,即没有打开不必要的文件描述符,也不会错误的使用大块的堆内存。
- 选择哪个子进程一般有两种方式
- 主进程主动选择,最简单的就是随机算法和Robin(轮流算法)
- 通过一个共享的工作队列来进行同步
第三篇 高性能服务器优化与检测
第十六章 服务器调制、调试和测试
服务器优化可以从3个方面,系统调制、服务器调试和压力测试
文件描述符是服务器程序的宝贵资源。应该总是关闭不在使用的文件描述符,比如,守护进程运行的服务器程序应该总是关闭标准输入、标准输出和标准错误这3个文件描述符。
调整最大文件描述符
ulimit -n
查看用户级文件描述符的方法ulimit -SHn max-file-number
将文件描述符设定为max-file-number
,这是临时的,只在当前session中有效如果想永久在用户级中有效,在/etc/security/limits.conf中添加下面两项
hard nofile max-file-number
soft nofile max-file-number
如果想在系统级有效使用
sysctl -w fs.file-max=max-file-number
这是临时的如果想永久在用户级有效,在/etc/sysctl.conf中添加下面
- fs.file-max=max-file-number, 然后执行sysctl -p使其生效
几乎所有的内核模块,包含核心和驱动,都在/proc/sys文件系统下提供了某些配置文件,以供用户调整模块的属性和行为。与网络相关的主要由两个文件
- /proc/sys/fs 都与文件系统有关
- /proc/sys/net 都与网络参数有关
如何用gdb调试多进程,可以用下面两种思路
- 单独调试子进程,先运行服务器,然后找到pid之后,将其attach到gdb调试器上
- 使用调试器选项
follow-fork-mode
用gdb调试多线程程序,有一些命令
- info threads 显示当前可以调试的所有线程
- thread ID 调试目标ID指定的线程
- set scheduler-locking [off/on/step]
一个调试进程池和线程池的一个方法就是,先将池中的进程个数或线程个数减少到1,以观察逻辑是否正确,然后逐渐增加进程和线程的同步是否正确。
IO复用方式是施压程度最高的,因为线程和进程的调度本身也要占用一定的时间
第十七章 系统监测工具
- tcpdump 网络抓包工具
- lsof 列出当前系统打开的文件描述符的工具
- nc 用于快速构建网络连接
- strace 跟踪程序运行过程中执行的系统调用和接收到的信号
- netstat 网络信息统计
- vmstat 能够输出系统的各种资源的使用情况
- ifstat 简单的网络流量监控工具
- mpstat 实时的监测多处理器上每个CPU的使用情况
附录 有关程序命令
- arp -a 可以用来查看所有的arp缓存 -d 删除 -s 添加
- ipcs 查看所有的共享资源实例
- sysctl -a查看内核参数