美文网首页
读书笔记| 高性能linux服务器编程

读书笔记| 高性能linux服务器编程

作者: daydaygo | 来源:发表于2018-11-08 20:28 被阅读80次

    date: 2016-08-02 22:37

    来源: swoole - 学习Swoole需要掌握哪些基础知识: http://wiki.swoole.com/wiki/page/487.html - 评论君
    推荐书籍: <aix unix 系统管理/维护与高可用架构> <构建高可用linux服务器> <设计原本> <领域特定语言> <代码之殇>1

    1-4 tcp/ip 协议簇 与 各种重要网络协议

    tpc/ip 协议簇

    tcp/ip 协议簇

    数据链路层: ARP + RARP -> ip / 机器物理地址 相互转换
    网络层: 数据包的选路和转发(逐跳通信); ip -> 根据数据包的目的ip地址选择如何投递; icmp -> 检测网络连接p
    传输层: 端到端(end to end)通信; tcp + udp + sctp
    应用层: 处理应用程序逻辑; ping telnet ospf dns

    封装

    tcp 报文封装

    应用层 -> send/write -> 传输层: tcp 报文/ udp 数据包(datagram) -> 网络层: ip 数据报 -> 数据链路层: 帧(frame, 帧的最大传输单元 max transmit unit, MTU)
    分用: 数据链路层 -> 应用层 的过程中, 每层交界都面对多个不同的协议, 根据数据报头部的字段需要分发给哪个协议来继续执行
    MTU: 可以使用 ifconfig/netstat 查看

    ARP: ip地址/物理地址 相互映射; 维护一个高速缓存, 包含 经常访问/最近访问
    DNS: 域名/ip 相互映射, 协议中包含查询类型, 比如 cname

    # arp
    arp -d 192.168.1.2 # 清除缓存
    tcpdump -i eth0 -ent '(dst 192.168.1.2 and src 192.168.1.3)or(dst 192.168.1.3 and src 192.168.1.2)' # 在 1.3 上面抓包
    telnet 192.168.1.2 echo # 用 1.3 来连接 1.2, 这样 tcpdump 中就可以抓到 arp 包了
    
    # dns
    /etc/resolv.conf # dns 服务器的ip
    host -t A baidu.com # 查询 baidu.com 的 ip
    tcpdump -i eth0 -nt -s 500 port domain # 使用 port domain 过滤, 只使用域名服务的包
    

    ip 协议

    为上层提供 无状态/无连接/不可靠 服务
    无状态: 数据报 相互独立/没有上下文关系 -> 无法 处理乱序/重复; 简单/高效
    无连接: ip通信双方都不长久的维持双方的任何信息, 上层协议需要指明ip地址
    不可靠: 不能保证ip数据报准确到达

    ipv4头部结构

    服务类型: 最小延时(ssh/telnet) 最大吞吐量(ftp) 最高可靠性 最小费用
    ip分片: ip数据报 > MTU

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/ip-module.png

    查看路由表: netstat/route
    ip 转发: 主机一般只能 发送/接收 数据报, 配置在 /proc/sys/net/ipv4/ip_forward
    重定向: icmp 重定向报文; 主机重定向

    ipv6: ipv4 地址不够用; 多播和流 / 自动配置, 便于管理 / 网络安全功能

    tcpdump -ntx -i lo # 抓取本地回路上的数据包, -x 输出数据包的二进制码
    telnet 127.0.0.1
    
    tcpdump -ntv -i eth0 icmp # 只抓取 icmp 报文
    ping baidu.com -s 1473 # -s 指定发送数据字节大小
    
    route add -host 192.168.1.2 dev eth0 # 发送到 1.2 的数据包直接结果 eth0 传送
    route del -net 192.168.1.0 netmask 255.255.255.0 # 无法访问同一个局域网的其他机器
    route del default # 删除默认路由, 结果就是无法访问外网了
    route add defalut gw 192.168.1.2 dev eth0 # 重设默认路由, 但是将网关设为某台主机, 而非路由
    
    route -Cn # 查看路由缓冲
    

    tcp 协议

    tcp vs udp: 面向连接 字节流 可靠传输
    tcp: 建立连接(一对一, 无法广播和多播) -> 分配必要内核资源以管理连接状态和连接上的数据传输 -> 全双工 -> 都必须断开连接释放系统资源
    MSS: max segment size, 最大报文长度, 通常设置为 MTU-40
    半关闭状态: 发送 FIN 并得到确认, 就由 连接状态 进入 半关闭状态, 此时还可以接受对方的数据, 直到对方发送 FIN, 连接关闭
    断线重连: 次数由 /proc/sys/net/ipv4/tcp_syn_retries 配置, 由1s开始, 每次重试时间 x2
    复位报文段(RST): 访问不存在的端口; 异常中止连接; 处理半打开连接
    按照数据长度: 交互数据(ssh/telnet, nagle算法, 通信双方任何时刻都最多只能发送一个未被确认的tcp报文段) 块状数据(ftp)
    超时重传: 超时时间 + 重传次数

    拥塞控制: 慢启动(slow start) 拥塞避免(congestion avoidance) 快速重传(fast retransmit) 快速恢复(fast recovery); 算法 -> reno vegas cubic

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/tcp-state-transition.png

    udp: 非常适合做广播和多播

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/tcp vs udp.png

    tcpdump -i eth0 -nt '(dst 192.168.1.2 and src 192.168.1.3)or(dst 192.168.1.3 and src 192.168.1.2)' # 查看 tcp 连接的 建立/关闭
    telnet 192.168.1.2 80
    
    nc -p 12345 192.168.1.2 80 # 测试 tcp TIME_WAIT 状态
    ctrl-c # 中断连接
    nc -p 12345 192.168.1.2 80 # 重新建立, 显示连接失败
    netstat -nat # 查看连接状态, 此时就处于 TIME_WAIT 状态
    
    iperf -s # 1.2, iperf 是一个衡量网络状况的工具, -s 表示作为服务器运行, 默认监听5001端口, 并丢弃该端口接受的所有数据
    telnet 192.168.1.2 5001 # 1.3 连接 iperf
    tcpdump -n -i eth0 port 5001
    

    tcp/ip 通信案例: 访问 Internet 上的 web 服务器

    正向代理: 客户端配置, 通过正向代理访问其他网络
    反向代理: 服务器配置, 接收用户请求, 分发给其他服务器处理

    tcpdump 抓一次 wget: 代理服务器 -> dns 服务器(dns); 代理服务器 -> 查询路由器MAC地址(arp); wegt -> 代理服务器(http); 代理服务器 -> web 服务器(http)

    http 请求(request): 请求行(请求方法 + 资源地址) + 请求头部(header) + 空行(<CR><LF>, 标识头部结束)
    http 应答(response): 状态行 + 应答头部
    http 无状态 -> cookie -> 标识 不同客户端

    /etc/init.d/ # 服务器程序存放地址
    service start|stop|restart xxx # 服务管理
    
    /etc/hosts # dns 查询 Internet 上的域名, 本地名称查询使用 hosts 文件
    /etc/host.conf # 自定义系统解析主机名
        order hosts,bind    # 先本地, 后dns
        multi on            # 允许匹配到多个ip
    

    高性能服务器编程

    C语言函数常见套路: 成功返回 0, 失败返回 -1 并设置 errno; 使用 bit 里标识状态(节约空间, 位运算也更快), 然后使用 掩码 来改变值(位运算, 比如 改变状态/ip地址转换); 使用正负来处理有限状态, 避免使用更多参数

    linux 网络编程基础 api

    字节序: cpu累加器 一般能加载超过一个字节, 所以字节的顺序, 会影响加载的整数的值
    大端序(big endian): 高位存储在高地址; 网络; java虚拟机
    小端序(little endian): 高位存储在低地址; 现代 pc 大部分采取

    协议族(protocol family) + 地址族(address family): unix(本地协议族) inet inet6
    ip 地址转换函数: char <-> int

    socket 选项

    inet_aton() inet_ntoa() # ipv4
    inet_pton() inet_ntop() # ipv4 + ipv6
    
    int socket(int domain, int type, int protocol) # 创建socket; domain->协议族 type->流/数据报 protocol->0(默认); 返回 socket 文件描述法
    int bind() # 命名socket, 绑定到地址族中的具体socket地址
    
    int listen(int socketfd, int backlog) # 监听socket, 不能马上接听客户连接, 要创建一个监听队列来存放待处理客户连接
    int accept() # 从监听队列接受一个连接
    
    int connect() # 客户端主动与服务器建立连接
    
    int close(int fd) # 将 fd 引用计数减一
    int shutdown(int socketfd, int howto) # 关闭 socket 的行为: r/w
    
    ssize_t recv(int socketfd, void *buf, size_t len, int flags) # 读取
    ssize_t send(int socketfd, const void *buf, size_t len, int flags) # 发送
    recvfrom() sendto() # udp
    recvmsg() sendmsg() # 通用: tcp + udp
    socketmark(int socketfd) # tcp 带外数据接收方法
    
    getsockname() / getpeername() # 获取一个socket的 本/远 端socket地址
    getsocketopt() / setsocketopt() # 获取/设置 socket 文件描述符
    
    gethostbyname() / gethostbyaddr()
    getservbyname() / getservbyport()
    getaddrinfo() # gethostbyname() + getservbyname()
    getnameinfo() # gethostbyaddr() + getservbyaddr()
    

    高级 io 函数

    fcntl(file control) 函数: 提供了对 fd 的各种控制操作; 通常用来将一个 fd 设置为 非阻塞

    int pipe(int fd[2]) # 创建一个管道, 以实现进程间通信; 单向, read/write 阻塞; 建立的管道也有数据大小
    
    int dup(int file_descriptor) # stdin -> 文件 / stdout -> 网络连接(如: cgi编程)
    int dup2(int file_descriptor, int file_descriptor_two)
    
    ssize_t readv(int fd, const struct iovec* vector, int count) # 分散写
    ssize_t writev(int fd, const struct iovec* vector, int count) # 集中读
    
    sendfile() # 在2个文件描述符之间直接传递数据(完全在内核中), 避免内核缓冲区和用户缓冲区之间的数据拷贝 -> 零拷贝
    
    mmap() # 申请一段内存 -> 进程间通信的共享内存 / 直接将文件映射到其中
    munmap() # 释放由 mmap() 创建的内存
    splice() # 在2个 fd 之间移动数据, 也是 零拷贝
    tee() # 2个 管道fd 之间复制数据, 也是 零拷贝
    
    int fcntl(int fd, int cmd, ...) # fcntl 函数
    

    linux 服务器程序规范

    一般以后台进程形式运行(也称为守护进程 daemon): 没有控制终端, 因而不会意外接受用户输入; 父进程通常为 init 进程(pid=1)
    通常有一套日志系统 -> 文件 / udp服务器 / /var/log 下拥有自己日志目录
    一般以某个非root用户运行: mysqld -> mysql; httpd -> apche; syslogd -> syslog
    通常是可配置的 -> /etc
    通常会在启动时生成一个 pid 文件并存入 /var/run 目录中, 比如 syslogd -> /var/run/syslogd.pid
    通常需要考虑 系统资源和限制, 以预测自身能承受多大负荷, 比如 fd总数/内存

    linux 系统日志: rsyslogd

    用户信息: 大部分服务器程序以 root 启动, 但不以 root 运行; uid euid gid egid
    euid: 使得运行用户拥有改程序的有效用户的权限, 方便资源访问; 比如 su 程序被设置了 set-user-id 标记

    进程间关系: 进程组(pgid, 每个进程都隶属一个进程组); 会话(session, 一些有关联的会话形成一个会话)

    系统资源: 物理设备(cpu, 内存) 系统策略限制(cpu时间) 具体实现限制(文件名长度限制)

    void syslog(int priority, const char* message, ...) # 和 rsyslosd 通信
    priority:
    LOG_EMEGE       0 系统不可用
    LOG_ALERT       1 报警, 需要立即采取行动
    LOG_CRIT        2 非常严重的情况
    LOG_ERR         3 错误
    LOG_WARNING     4 警告
    LOG_NOTICE      5 通知
    LOG_INFO        6 信息
    LOG_DEBUG       7 调试
    
    void openlog(const char* ident, int logopt, int facility) # 改变 syslog 默认的输出方式, 进一步结构化日志内容
    logopt:
    LOG_PID         0x01 在日志消息中包含程序 pid
    LOG_CONS        0x02 如果无法记录到日志文件, 则打印到终端
    LOG_ODELAY      0x04 延迟打开日志功能, 直到第一次调用 syslog
    LOG_NDELAY      0x08 不延迟打开日志功能
    
    int setlogmask(int maskpri) # 简单设置日志掩码, 使日志级别大于日志掩码的日志信息被系统忽略
    int closelog()
    
    getuid() / setuid() # 用户身份相关函数
    
    getpgid() / setpgid() # 进程组
    setsid() / getsid() # 会话, 使用调用进程 pid 作为 sid
    
    ps -o pid,ppid,pgid,sid,comm | less # 使用 ps 查看
    
    setrlimit() / getrilimit() # 系统资源
    
    getcwd() / chdir() / chroot() # 获取工作目录 / 改变进程工作目录 / 改变进程根目录
    
    int daemon(int nochdir, int noclose) # 服务器程序后台化
    

    高性能服务器程序框架

    3个主要模块: io 处理单元(4种io处理模式+2种高效事件处理模式); 逻辑单元(2种高效并发模式+有限状态机); 存储单元(可选, 和网络编程无关)
    c/s模型: client-server, server为中心

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/tcp-workflow.png

    p2p(peer to peer, 点对点)模型: 优点 -> 每台机器消耗服务的同时也给别人提供服务; 缺点 -> 用户之间传输的请求过多时, 网络负载将加重 / 主机之间很难互相发现, 需要带有一个发现服务器; 其实每个点既是 服务器 也是 客户端, 也是采用 c/s 模型实现

    io处理单元: 服务器管理客户连接
    逻辑单元: 通常一个 进程/线程, 分析并处理客户数据, 然后将结果传递给 io 处理单元
    网络存储单元: DB / cache / file
    请求队列: 各单元通信的抽象

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/server-basic-framework.png

    4种io模型

    socket 基本api中可能被阻塞的系统调用: accept send recv connect
    非阻塞io通常要和其他其他io通知机制一起使用, 比如 io复用 / sigio信号
    io复用(最常使用): 应用程序通过 io复用函数 向内核注册一组事件, 内核通过 io复用函数 把其中的就绪事件通知给应用程序
    linux常用 io复用函数: select poll epoll_wait; 本身是阻塞的, 具有同时监听多个io事件的能力
    sigio 信号: 为一个 fd 指定 宿主进程 -> fd 上有事件发生 -> sigio 信号处理函数被触发 -> 被指定的宿主程序捕获到 sigio信号

    理论上 阻塞io / io复用 / 信号驱动io 都是 同步io模型: 先 io(就绪)事件, 后 io读写
    异步io(aio.h): 用户直接对io执行读写操作 -> 内核完成io操作 -> 通知应用程序 io完成事件

    同步 vs 异步: 内核向应用程序通知的时间(就绪事件 vs 完成时间) 由谁来完成io读写(应用程序 vs 内核)

    2种高效事件处理模式

    reactor模式(同步io): 主线程(io处理单元) 只负责监听 fd 上的事件, 有就通知 工作线程(逻辑单元), 不做其他实质性工作; 工作线程完成 读写数据/接受连接/处理连接 等

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/reactor.png

    proactor模式(异步io): 所有io操作交给主线程和内核, 工作线程只负责业务逻辑; 更符合服务器编程框架

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/proactor.png

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/monitor-proactor.png

    2种高效并发模式

    并发编程: 如果是计算密集型, 并发编程没有优势, 反而由于任务切换使效率降低; io密集型, io操作速度 远小于 cpu计算速度

    半同步/半异步模式: 同步 -> 程序完全按照代码顺序执行, 异步 -> 程序执行由系统事件(中断/信号)来驱动; 同步 -> 逻辑单元, 异步 -> io单元
    半同步/半反应堆(half-sync/half-reactive)模式: 主线程(异步, 监听/连接 socket) -> 请求队列 -> 工作线程(获取连接socket); 缺点 -> 共享请求队列, 需要加锁 / 每个工作线程同一时间只能处理一个客户请求, 增加工作进程会增加工作线程切换开销
    高效 半同步/半异步模式: 主线程 只监听socket -> 派发新请求给 工作进程 -> 工作进程 连接socket/处理io/维持自己的事件循环

    领导者/追随者模式: 多个工作现成轮流获得事件源集合, 轮流监听/分发并处理事件; 1个领导者 + 多个追随者(线程池, 休眠) -> 领导者监听到io事件 -> 自己处理io事件/线程池中选出新的领导者

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/leader-follower.png

    有限状态机 finite state machine

    提供服务器性能的其他建议

    池(pool): 服务器硬件资源相对 充裕 -> 空间换时间; 一组资源集合, 服务器启动之初就完全创建并初始化, 这样就成为了 静态资源, 服务器正式运行时, 需要相关资源可以直接从池中获取, 无须动态分配, 使用完后可以直接把资源放回池中, 无须执行系统调用来释放资源; 分配 足够多 + 动态分配; 内存池 / 进程池 / 线程池 / 连接池
    内存池: socket 接收缓存/发送缓存
    进程池/线程池 -> 并发编程, 无须调用 fork/pthread_create
    连接池: 服务器/服务器机群的内部永久连接, 比如 db连接池

    数据复制: 避免不必要的数据复制
    内存缓冲区 -> 用户程序缓冲区: 内核 直接处理, 比如 ftp服务器中使用 零拷贝 函数 sendfile()
    用户代码内 -> 比如2个进程间要传递大量数据, 应该考虑 共享内存, 而不是 管道/消息队列

    上线文切换(context switch): 进程切换/线程切换 导致的 系统开销
    共享资源加锁保护: 锁 -> 不处理任何业务逻辑, 而且需要访问内核资源 -> 如果有更好的解决方案, 就应该避免使用锁 / 如果必须使用锁, 应尽量减少锁的粒度

    io复用

    io复用: 程序同时监听多个 fd; 本身是阻塞的

    使用 io 复用的场景:

    • client需要同时监听多个 socket
    • client需要同时处理用户输入和网络连接
    • tcp server需要同时处理 socket 监听/连接 -> io复用最多的场合
    • server 需要同时处理 tcp/udp, 比如 回射 server
    • server 需要 监听多个端口/处理多个服务, 比如 xinetd server

    select 系统调用用途: 在一段指定时间内, 监听用户感兴趣的 fd 上的 read/write/exception 事件; fd 就绪条件: 可读 -> balabala; 可写 -> balabala; 处理带外数据
    poll 系统调用: 和 select 类似, 在一段时间内轮询一定数量的 fd, 以测试其中是否有就绪者

    epoll 系统调用

    使用一组函数来完成
    把用户关心的 fd 上的事件放在内核的一个事件表中
    LT(level trigger, 电平触发): 默认, 相当于一个效率较高的 poll; epoll_wait 检测到事件时就通知应用程序, 应用程序可以不立即处理(因为会重复通知)
    ET(edge trigger, 边沿触发): epoll的高效工作模式; epoll_wait 检测到事件时就通知应用程序, 应用程序必须立即处理(因为后序不会再通知), 减低了同一个 epoll 事件被重复触发的次数
    EPOLLONESHOT 事件: 一个 socket 连接在任一时刻都只被一个线程处理; 保证了连接完整性, 避免很多可能的竞态条件

    int epoll_create(int size)  # 创建额外的 fd, 用来标识内核中的事件表
    int epoll_ctl()             # 操作内核事件表
    int epoll_wait()            # 在一段超时时间内等待一组 fd 上的事件 -> 只传递就绪事件, 不处理用户注册的事件
    

    http://qiniu.daydaygo.top/high-performance-linux-server-programming/select-poll-epoll.png

    信号

    用户/系统/进程 发送给目标进程的信息, 以通知目标进程 某个状态的改变/系统异常
    被挂起的信号: 设置进程信号掩码 -> 屏蔽信号(程序不用处理所有的信号)
    统一事件源: 信号事件/io事件一样被处理; 信号事件 -> 管道 -> 监听管道读端fd 上的可读事件 -> io事件; 如 libevent 库

    linux信号可由如下条件产生:

    • 对于前台进程, 可以通过输入特殊的终端字符来发送信号, 如 Ctrl-C 通常会发送一个中断信号
    • 系统异常, 如 浮点异常/非法内存段访问
    • 系统状态变化, 如 alarm定时器到期 -> SIGALRM 信号
    • 运行kill命令/调用kill函数
    int kill(pid_t pid, int sig)            # 发送信号; pid -> pid/本进程组/除init进程外/其他进程组; sig -> 都大于0
    int signal()                            # 为一个信号设置处理函数
    int sigaction()                         # 更健壮的接口
    int sigpending()                        # 获得当前进程被挂起的信号
    

    网络编程相关信号:

    • SIGHUP: 挂起进程的控制终端; 没有控制终端的网络后台程序 -> 强制服务器重读配置文件
    • SIGPIPE: 向 读端关闭的 管道/socket 写数据 -> 默认关闭进程 -> 不希望错误的写操作而导致程序退出 -> 代码中捕获并处理该信号/至少忽略
    • SIGURG: 内核通知应用程序带外数据到达 -> io复用/SIGURG信号

    定时器

    需要处理的第三类事件 - 定时事件: 如 定时检测一个客户连接的活动状态; 有效组织, 预期触发 + 不影响主要逻辑
    定时事件 -> 封装成 定时器 -> 使用某种容器类数据结构(2种高效管理定时器的容器: 时间轮/时间堆) -> 将所有定时器串联起来 -> 实现对定时事件的统一管理
    linux 3种定时方法: socket选项 SO_RECVTIEMO/SO_SENDTIEMO; SIGALRM 信号; io复用超时参数
    定时器至少包含2个成员: 超时时间(绝对/相对) + 任务回调函数

    定时tick: 使用固定频率心搏函数tick -> 依次检测到期定时器 -> 执行定时器上的回调函数(时间轮)
    最短时间tick: 每次使用最小定时器超时值作为tick -> tick调用 -> 最小定时器被调用 -> 更新剩余定时器(时间堆)

    简单时间轮: 每个槽(slot)用有相同的槽间隔si(slot interval, 其实就是心搏时间tick); 每个槽放在一个 定时器链表, 新的定时器分配通过定时时间hash到不同的槽
    复杂时间轮 -> 类似 水表, 有不同精度的轮子
    时间堆: 最小堆实现

    高性能io框架库 - Libevent

    linux服务器必须处理的3类事件: io事件/信号/定时事件
    处理事件需要考虑的问题: 统一事件源 -> io复用; 可移植性; 对并发编程的支持, 避免竞态条件

    句柄(handle): 统一事件源 -> 绑定句柄 -> 内核检测到就绪事件 -> 通过句柄通知应用程序
    事件循环: 无法预知客户 连接请求/暂停信号 -> 循环等待并处理
    事件多路分发器(EventDemultiplexer): 封装各种 io复用系统 为统一的接口
    具体事件处理器: 框架提供一个接口, 应用程序来自己扩展

    Libevent 源码分析:

    • 跨平台
    • 统一事件源
    • 线程安全, 使用 libevent_pthreads
    • 基于 Reactor 模式实现
    • 编写产品级函数库需要考虑哪些细节
    • 提高C语言功底: 大量函数指针 + 多态机制 + 一些基础数据结构的高效实现

    多进程编程

    多进程编程内容:

    • fork系统调用 -> 复制进程映像; exec系统调用 -> 替换进程映像
    • 僵尸进程以及如何避免
    • 进程间通信(inter-process communication, IPC)最简单的方式: 管道
    • 3种System V IPC: 信号量/消息队列/共享内存
    • 进程间传递fd的通用做法: 通过unix本地域socket传递特殊的辅助数据

    fork 系统调用: 创建进程; 每次调用返回2次 -> 父进程返回子进程pid, 子进程返回0; 写时复制(copy on write)
    exec 系统调用: 子进程中执行其他程序; 原程序已经被exec参数指定的程序完全替换(代码+数据) -> 原程序在exec调用之后的代码都不会执行

    僵尸态 1: 父进程一般需要跟踪子进程退出状态, 所以子进程退出时内核不会立即释放该进程进程表表项 -> 子进程退出之后, 父进程读取其退出状态之前
    僵尸态 2: 父进程异常 结束/异常终止, 子进程继续运行, os将其ppid设置为1(init进程) -> 父进程退出之后, 子进程退出之前

    访问共享资源的代码 -> 关键代码段/临界区
    进程同步问题 -> 同一个时刻只有一个进程可以拥有对资源的独占式访问 -> 确保任一时刻只有一个进程能进入关键代码段
    信号量(semaphore): 特殊的变量, 只能取自然数值并只支持2种操作 P/V 操作

    共享内存: 最高效IPC机制; 需要辅助手段同步进程对共享内存的访问; 通常和其他IPC方式一起使用;
    api1: sys/shm.h -> shmget/shmat/shmdt/shmctl
    api2: mmap() + 打开同一个文件 -> 无关进程之间共享内存
    实例: 聊天室服务器

    管道/命名管道: 必须 FIFO 方式接收数据
    消息队列: 在2个进程之间传递二进制数据块的一种简单有效方式; 每个数据块包含特定type -> 接收方有选择的接收数据
    api: sys/msg.h -> msgget() / msgsnd / msgrcv / msgctl

    传递fd: 接收进程创建一个新的fd -> 发送进程和新进程的 fd 都执行内核中形同的文件表项

    pid_t fork(void)
    
    # 退出进程, 避免僵尸进程
    wait()                  # 将进程阻塞, 直到某个子进程结束运行
    waitpid()               # 只等待pid参数指定的子进程
    
    # 管道
    pipe()                  # 创建管道
    socketpair()            # 创建全双工管道
    
    # 信号量 P/V操作
    P(sv): sv>0 -> --sv; sv==0 -> 挂起
    V(sv): 其他进程等待 sv -> 唤醒; 没有, ++sv
    
    # 信号量系统调用
    int semget()            # 创建新的信号量集
    int semop()             # 改变信号量, 即 P/V 操作
    int semctl()            # 允许调用者对信号量进行直接控制
    
    # 共享内存系统调用
    int shmget()            # 创建/获取 共享内存
    void shmat()            # 关联 共享内存 到进程的地址空间
    int shmdt()             # 从进程地址空间 分离 共享内存
    int shmctl()            # 控制 共享内存 某些属性
    
    # ipc 相关命令
    ipcs                    # 列出os中共享资源
    ipcrm                   # 删除遗留在os中的共享资源
    

    多线程编程

    linux线程库 -> NPTL(native POSIX thread library)

    POSIX 线程(简称 pthread)标准:

    • 创建/结束 线程
    • 读取/设置 线程属性
    • 同步方式: POSIX信号量 / 互斥锁 / 条件变量

    根据运行环境和调度者: 内核线程 -> 运行在内核空间, 由内核来调度; 用户线程 -> 运行在用户空间, 由线程库来调度; 内核线程 作为 用户线程 运行的容器
    完全在用户空间实现的线程; 无须内核支持; 对应一个内核线程; 优点 -> 创建/调度 快, 不占用额外系统资源; 缺点 -> 一个进程的多个线程无法运行在不同 cpu 上
    完全由内核调度: 1:1 映射用户空间线程和内核线程
    双层调度: 混合上面2种

    pthread api: pthread.h

    POSIX 信号量: 和IPC中的信号量定义一样; api -> semaphore.h
    互斥锁: 同步线程对共享数据的访问; 用来保护关键代码块(加锁/解锁), 类似二进制信号量; api -> pthread.h -> pthread_mutex_*
    互斥锁死锁: 对一个已经加锁的普通锁再次加锁 -> 如设计不够仔细的递归函数; 2个线程按照不同的顺序唉申请2个互斥锁
    条件变量: 线程之间同步共享数据的值; api -> pthread.h -> pthread_cond_*

    可重入函数: 能被多个线程同时调用且不发生静态条件 -> 线程安全(thread safe); linux库函数只有一小部分不可重入; 不可重入函数的重入版本 -> 函数末尾加 _r

    # pthread
    pthread_read()              # 创建
    pthread_exit()              # 最好调用此函数, 以确保安全/干净地退出
    pthread_join()              # 同一个进程的所有进程都可以调用此函数来回收其他线程
    pthread_cancel()            # 异常终止一个线程
    
    pthread_atfork()            # 确保fork调用后父进程和子进程都拥有一个清楚的锁状态
    pthread_sigmask()           # 每个线程可以独立的设置信号掩码
    

    进程池和线程池

    动态创建缺点: 耗时, 响应慢; 动态创建 子进程/子线程 为一个客户服务会产生大量细微 进程/线程, 进程/线程 切换将消耗大量cpu; 动态创建 子进程 是当前进程的完整映像, 必须警慎管理分配的 fd 等系统资源

    进程池: 建立主进程 -> 主进程建立主进程 -> 新任务 -> 分配任务 -> 通知机制(主进程传递信息给子进程)
    分配任务方式: 主动选择 -> 随机算法/轮流选取(round robin) -> 均衡分配; 工作队列
    通知机制: 预先建立管道; 父子线程之间只需要把这些数据定义为全局即可
    处理多用户: 并发模式选择; 常连接(一个客户多次请求可以复用一个tcp连接); 同一个客户是否由同一个进程来处理

    高性能服务器优化与监控

    服务器 调制/调试/测试

    系统配置调制

    fd: 几乎所有的系统调用都是和 fd 打交道, 而系统的分配的 fd数量 是有限制的, 所以我们总是要关闭那些不在使用的 fd, 以释放占用的资源, 如 守护进程关闭 stdio/stderr
    最大 fd 限制: 用户级 + 系统级
    内核模块

    ulimit -n                               # 查看用户级 fd 限制
    ulimit -SHn mak-file-number             # (临时)修改用户级 fd 限制为 max-file-number
    /etc/security/limits.conf               # (永久)
    sysctl -w fs.file-max=max-file-number   # (临时)修改系统级 fd 限制为 max-file-number
    /etc/sysctl.conf                        # (永久)
    
    sysctl -a                               # 查看下面这些内核参数
    /proc/sys                               # 几乎所有的 内核模块+驱动程序 都在此文件系统下提供了某些配置文件以供用户调整模块的属性和行为
        fs/                                 # 文件系统相关
            file-max                        # 系统级 fd 限制(临时修改)
            innode-max                      # 应当设置为 file-max 的 3-4 倍
            epoll/max_user_wathes           # 一个用户能忘epoll内核事件表注册的事件总量
        net/                                # 网络模块
            core/
                somaxconn                   # listen监听队列: ESTABLISH最大数量
            ipv4/
                tcp_max_syn_backlog         # listen监听队列: ESTABLISH+SYN_RCVD 最大数量
                tcp_wmem                    # 一个socket的tcp写缓冲区 min/default/max
                tcp_rmem                    # 一个socket的tcp读缓冲区 min/default/max -> 接收通告窗口
                tcp_syncookies              # 是否打开tcp同步标签(tcp_syncookies)
            ipv6/
    

    gdb调试

    # 调试子进程
    ps -ef|grep cgisrv
    gdb
    (gdb) gdb attach 4183 # 附加子进程
    (gdb) b processpool.h:264 # 设置子进程中的断点
    
    (gdb) set follow-fork-mode parent/child # 程序执行 fork 系统调用后调试 父/子 进程
    
    # 调试多线程程序
    info threads # 显示当前可调试的所有线程, 线程会带上id
    thread id # 调试指定id的线程
    set scheduler-locking off/on/step # 是否只让当前调试进程运行
    

    压力测试

    思路: 使用 epool 实现io复用来模仿压力

    系统监控工具

    tcpdump: tcp 抓包工具
    lsof(list open file): 查看打开的 fd
    nc(netcat): 快速构建网络连接
    strace: 测试服务器性能的重要工具
    netstat: 网络信息统计
    vmstat(virtual memory statistic): 实时输出系统各种资源的使用情况
    ifstat(interface statistic): 简单的网络流量监测工具
    mpstat(multi-processor statistic): 实时监测多处理器系统上的每个 cpu 使用情况

    # 使用表达式进一步过滤数据
    tcpdump net 1.2.3.0/24 # type: host net port portrange
    tcpdump dst port 13679 # dir(方向): src dst
    tcpdump icmp # proto(协议): icmp tcp udp ip
    tcpdump ip host a and not b # 支持 逻辑运算
    
    # lsof
    lsof -i@192.168.1.108:22 # 连接到 ssh 的 socket fd
    lsof -c websrv # 查看 websrv 程序打开了哪些 fd
    

    相关文章

      网友评论

          本文标题:读书笔记| 高性能linux服务器编程

          本文链接:https://www.haomeiwen.com/subject/pfrgxqtx.html