讀書筆記| 高性能linux服務(wù)器編程

date: 2016-08-02 22:37

來源: swoole - 學習Swoole需要掌握哪些基礎(chǔ)知識: http://wiki.swoole.com/wiki/page/487.html - 評論君
推薦書籍: <aix unix 系統(tǒng)管理/維護與高可用架構(gòu)> <構(gòu)建高可用linux服務(wù)器> <設(shè)計原本> <領(lǐng)域特定語言> <代碼之殤>1

1-4 tcp/ip 協(xié)議簇 與 各種重要網(wǎng)絡(luò)協(xié)議

tpc/ip 協(xié)議簇

tcp/ip 協(xié)議簇

數(shù)據(jù)鏈路層: ARP + RARP -> ip / 機器物理地址 相互轉(zhuǎn)換
網(wǎng)絡(luò)層: 數(shù)據(jù)包的選路和轉(zhuǎn)發(fā)(逐跳通信); ip -> 根據(jù)數(shù)據(jù)包的目的ip地址選擇如何投遞; icmp -> 檢測網(wǎng)絡(luò)連接p
傳輸層: 端到端(end to end)通信; tcp + udp + sctp
應(yīng)用層: 處理應(yīng)用程序邏輯; ping telnet ospf dns

封裝

tcp 報文封裝

應(yīng)用層 -> send/write -> 傳輸層: tcp 報文/ udp 數(shù)據(jù)包(datagram) -> 網(wǎng)絡(luò)層: ip 數(shù)據(jù)報 -> 數(shù)據(jù)鏈路層: 幀(frame, 幀的最大傳輸單元 max transmit unit, MTU)
分用: 數(shù)據(jù)鏈路層 -> 應(yīng)用層 的過程中, 每層交界都面對多個不同的協(xié)議, 根據(jù)數(shù)據(jù)報頭部的字段需要分發(fā)給哪個協(xié)議來繼續(xù)執(zhí)行
MTU: 可以使用 ifconfig/netstat 查看

ARP: ip地址/物理地址 相互映射; 維護一個高速緩存, 包含 經(jīng)常訪問/最近訪問
DNS: 域名/ip 相互映射, 協(xié)議中包含查詢類型, 比如 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 服務(wù)器的ip
host -t A baidu.com # 查詢 baidu.com 的 ip
tcpdump -i eth0 -nt -s 500 port domain # 使用 port domain 過濾, 只使用域名服務(wù)的包

ip 協(xié)議

為上層提供 無狀態(tài)/無連接/不可靠 服務(wù)
無狀態(tài): 數(shù)據(jù)報 相互獨立/沒有上下文關(guān)系 -> 無法 處理亂序/重復; 簡單/高效
無連接: ip通信雙方都不長久的維持雙方的任何信息, 上層協(xié)議需要指明ip地址
不可靠: 不能保證ip數(shù)據(jù)報準確到達

ipv4頭部結(jié)構(gòu)

服務(wù)類型: 最小延時(ssh/telnet) 最大吞吐量(ftp) 最高可靠性 最小費用
ip分片: ip數(shù)據(jù)報 > MTU

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

查看路由表: netstat/route
ip 轉(zhuǎn)發(fā): 主機一般只能 發(fā)送/接收 數(shù)據(jù)報, 配置在 /proc/sys/net/ipv4/ip_forward
重定向: icmp 重定向報文; 主機重定向

ipv6: ipv4 地址不夠用; 多播和流 / 自動配置, 便于管理 / 網(wǎng)絡(luò)安全功能

tcpdump -ntx -i lo # 抓取本地回路上的數(shù)據(jù)包, -x 輸出數(shù)據(jù)包的二進制碼
telnet 127.0.0.1

tcpdump -ntv -i eth0 icmp # 只抓取 icmp 報文
ping baidu.com -s 1473 # -s 指定發(fā)送數(shù)據(jù)字節(jié)大小

route add -host 192.168.1.2 dev eth0 # 發(fā)送到 1.2 的數(shù)據(jù)包直接結(jié)果 eth0 傳送
route del -net 192.168.1.0 netmask 255.255.255.0 # 無法訪問同一個局域網(wǎng)的其他機器
route del default # 刪除默認路由, 結(jié)果就是無法訪問外網(wǎng)了
route add defalut gw 192.168.1.2 dev eth0 # 重設(shè)默認路由, 但是將網(wǎng)關(guān)設(shè)為某臺主機, 而非路由

route -Cn # 查看路由緩沖

tcp 協(xié)議

tcp vs udp: 面向連接 字節(jié)流 可靠傳輸
tcp: 建立連接(一對一, 無法廣播和多播) -> 分配必要內(nèi)核資源以管理連接狀態(tài)和連接上的數(shù)據(jù)傳輸 -> 全雙工 -> 都必須斷開連接釋放系統(tǒng)資源
MSS: max segment size, 最大報文長度, 通常設(shè)置為 MTU-40
半關(guān)閉狀態(tài): 發(fā)送 FIN 并得到確認, 就由 連接狀態(tài) 進入 半關(guān)閉狀態(tài), 此時還可以接受對方的數(shù)據(jù), 直到對方發(fā)送 FIN, 連接關(guān)閉
斷線重連: 次數(shù)由 /proc/sys/net/ipv4/tcp_syn_retries 配置, 由1s開始, 每次重試時間 x2
復位報文段(RST): 訪問不存在的端口; 異常中止連接; 處理半打開連接
按照數(shù)據(jù)長度: 交互數(shù)據(jù)(ssh/telnet, nagle算法, 通信雙方任何時刻都最多只能發(fā)送一個未被確認的tcp報文段) 塊狀數(shù)據(jù)(ftp)
超時重傳: 超時時間 + 重傳次數(shù)

擁塞控制: 慢啟動(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 連接的 建立/關(guān)閉
telnet 192.168.1.2 80

nc -p 12345 192.168.1.2 80 # 測試 tcp TIME_WAIT 狀態(tài)
ctrl-c # 中斷連接
nc -p 12345 192.168.1.2 80 # 重新建立, 顯示連接失敗
netstat -nat # 查看連接狀態(tài), 此時就處于 TIME_WAIT 狀態(tài)

iperf -s # 1.2, iperf 是一個衡量網(wǎng)絡(luò)狀況的工具, -s 表示作為服務(wù)器運行, 默認監(jiān)聽5001端口, 并丟棄該端口接受的所有數(shù)據(jù)
telnet 192.168.1.2 5001 # 1.3 連接 iperf
tcpdump -n -i eth0 port 5001

tcp/ip 通信案例: 訪問 Internet 上的 web 服務(wù)器

正向代理: 客戶端配置, 通過正向代理訪問其他網(wǎng)絡(luò)
反向代理: 服務(wù)器配置, 接收用戶請求, 分發(fā)給其他服務(wù)器處理

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

http 請求(request): 請求行(請求方法 + 資源地址) + 請求頭部(header) + 空行(<CR><LF>, 標識頭部結(jié)束)
http 應(yīng)答(response): 狀態(tài)行 + 應(yīng)答頭部
http 無狀態(tài) -> cookie -> 標識 不同客戶端

/etc/init.d/ # 服務(wù)器程序存放地址
service start|stop|restart xxx # 服務(wù)管理

/etc/hosts # dns 查詢 Internet 上的域名, 本地名稱查詢使用 hosts 文件
/etc/host.conf # 自定義系統(tǒng)解析主機名
    order hosts,bind    # 先本地, 后dns
    multi on            # 允許匹配到多個ip

高性能服務(wù)器編程

C語言函數(shù)常見套路: 成功返回 0, 失敗返回 -1 并設(shè)置 errno; 使用 bit 里標識狀態(tài)(節(jié)約空間, 位運算也更快), 然后使用 掩碼 來改變值(位運算, 比如 改變狀態(tài)/ip地址轉(zhuǎn)換); 使用正負來處理有限狀態(tài), 避免使用更多參數(shù)

linux 網(wǎng)絡(luò)編程基礎(chǔ) api

字節(jié)序: cpu累加器 一般能加載超過一個字節(jié), 所以字節(jié)的順序, 會影響加載的整數(shù)的值
大端序(big endian): 高位存儲在高地址; 網(wǎng)絡(luò); java虛擬機
小端序(little endian): 高位存儲在低地址; 現(xiàn)代 pc 大部分采取

協(xié)議族(protocol family) + 地址族(address family): unix(本地協(xié)議族) inet inet6
ip 地址轉(zhuǎn)換函數(shù): char <-> int

socket 選項

inet_aton() inet_ntoa() # ipv4
inet_pton() inet_ntop() # ipv4 + ipv6

int socket(int domain, int type, int protocol) # 創(chuàng)建socket; domain->協(xié)議族 type->流/數(shù)據(jù)報 protocol->0(默認); 返回 socket 文件描述法
int bind() # 命名socket, 綁定到地址族中的具體socket地址

int listen(int socketfd, int backlog) # 監(jiān)聽socket, 不能馬上接聽客戶連接, 要創(chuàng)建一個監(jiān)聽隊列來存放待處理客戶連接
int accept() # 從監(jiān)聽隊列接受一個連接

int connect() # 客戶端主動與服務(wù)器建立連接

int close(int fd) # 將 fd 引用計數(shù)減一
int shutdown(int socketfd, int howto) # 關(guān)閉 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) # 發(fā)送
recvfrom() sendto() # udp
recvmsg() sendmsg() # 通用: tcp + udp
socketmark(int socketfd) # tcp 帶外數(shù)據(jù)接收方法

getsockname() / getpeername() # 獲取一個socket的 本/遠 端socket地址
getsocketopt() / setsocketopt() # 獲取/設(shè)置 socket 文件描述符

gethostbyname() / gethostbyaddr()
getservbyname() / getservbyport()
getaddrinfo() # gethostbyname() + getservbyname()
getnameinfo() # gethostbyaddr() + getservbyaddr()

高級 io 函數(shù)

fcntl(file control) 函數(shù): 提供了對 fd 的各種控制操作; 通常用來將一個 fd 設(shè)置為 非阻塞

int pipe(int fd[2]) # 創(chuàng)建一個管道, 以實現(xiàn)進程間通信; 單向, read/write 阻塞; 建立的管道也有數(shù)據(jù)大小

int dup(int file_descriptor) # stdin -> 文件 / stdout -> 網(wǎng)絡(luò)連接(如: 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個文件描述符之間直接傳遞數(shù)據(jù)(完全在內(nèi)核中), 避免內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的數(shù)據(jù)拷貝 -> 零拷貝

mmap() # 申請一段內(nèi)存 -> 進程間通信的共享內(nèi)存 / 直接將文件映射到其中
munmap() # 釋放由 mmap() 創(chuàng)建的內(nèi)存
splice() # 在2個 fd 之間移動數(shù)據(jù), 也是 零拷貝
tee() # 2個 管道fd 之間復制數(shù)據(jù), 也是 零拷貝

int fcntl(int fd, int cmd, ...) # fcntl 函數(shù)

linux 服務(wù)器程序規(guī)范

一般以后臺進程形式運行(也稱為守護進程 daemon): 沒有控制終端, 因而不會意外接受用戶輸入; 父進程通常為 init 進程(pid=1)
通常有一套日志系統(tǒng) -> 文件 / udp服務(wù)器 / /var/log 下?lián)碛凶约喝罩灸夸?br> 一般以某個非root用戶運行: mysqld -> mysql; httpd -> apche; syslogd -> syslog
通常是可配置的 -> /etc
通常會在啟動時生成一個 pid 文件并存入 /var/run 目錄中, 比如 syslogd -> /var/run/syslogd.pid
通常需要考慮 系統(tǒng)資源和限制, 以預測自身能承受多大負荷, 比如 fd總數(shù)/內(nèi)存

linux 系統(tǒng)日志: rsyslogd

用戶信息: 大部分服務(wù)器程序以 root 啟動, 但不以 root 運行; uid euid gid egid
euid: 使得運行用戶擁有改程序的有效用戶的權(quán)限, 方便資源訪問; 比如 su 程序被設(shè)置了 set-user-id 標記

進程間關(guān)系: 進程組(pgid, 每個進程都隸屬一個進程組); 會話(session, 一些有關(guān)聯(lián)的會話形成一個會話)

系統(tǒng)資源: 物理設(shè)備(cpu, 內(nèi)存) 系統(tǒng)策略限制(cpu時間) 具體實現(xiàn)限制(文件名長度限制)

void syslog(int priority, const char* message, ...) # 和 rsyslosd 通信
priority:
LOG_EMEGE       0 系統(tǒng)不可用
LOG_ALERT       1 報警, 需要立即采取行動
LOG_CRIT        2 非常嚴重的情況
LOG_ERR         3 錯誤
LOG_WARNING     4 警告
LOG_NOTICE      5 通知
LOG_INFO        6 信息
LOG_DEBUG       7 調(diào)試

void openlog(const char* ident, int logopt, int facility) # 改變 syslog 默認的輸出方式, 進一步結(jié)構(gòu)化日志內(nèi)容
logopt:
LOG_PID         0x01 在日志消息中包含程序 pid
LOG_CONS        0x02 如果無法記錄到日志文件, 則打印到終端
LOG_ODELAY      0x04 延遲打開日志功能, 直到第一次調(diào)用 syslog
LOG_NDELAY      0x08 不延遲打開日志功能

int setlogmask(int maskpri) # 簡單設(shè)置日志掩碼, 使日志級別大于日志掩碼的日志信息被系統(tǒng)忽略
int closelog()

getuid() / setuid() # 用戶身份相關(guān)函數(shù)

getpgid() / setpgid() # 進程組
setsid() / getsid() # 會話, 使用調(diào)用進程 pid 作為 sid

ps -o pid,ppid,pgid,sid,comm | less # 使用 ps 查看

setrlimit() / getrilimit() # 系統(tǒng)資源

getcwd() / chdir() / chroot() # 獲取工作目錄 / 改變進程工作目錄 / 改變進程根目錄

int daemon(int nochdir, int noclose) # 服務(wù)器程序后臺化

高性能服務(wù)器程序框架

3個主要模塊: io 處理單元(4種io處理模式+2種高效事件處理模式); 邏輯單元(2種高效并發(fā)模式+有限狀態(tài)機); 存儲單元(可選, 和網(wǎng)絡(luò)編程無關(guān))
c/s模型: client-server, server為中心

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

p2p(peer to peer, 點對點)模型: 優(yōu)點 -> 每臺機器消耗服務(wù)的同時也給別人提供服務(wù); 缺點 -> 用戶之間傳輸?shù)恼埱筮^多時, 網(wǎng)絡(luò)負載將加重 / 主機之間很難互相發(fā)現(xiàn), 需要帶有一個發(fā)現(xiàn)服務(wù)器; 其實每個點既是 服務(wù)器 也是 客戶端, 也是采用 c/s 模型實現(xiàn)

io處理單元: 服務(wù)器管理客戶連接
邏輯單元: 通常一個 進程/線程, 分析并處理客戶數(shù)據(jù), 然后將結(jié)果傳遞給 io 處理單元
網(wǎng)絡(luò)存儲單元: DB / cache / file
請求隊列: 各單元通信的抽象

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

4種io模型

socket 基本api中可能被阻塞的系統(tǒng)調(diào)用: accept send recv connect
非阻塞io通常要和其他其他io通知機制一起使用, 比如 io復用 / sigio信號
io復用(最常使用): 應(yīng)用程序通過 io復用函數(shù) 向內(nèi)核注冊一組事件, 內(nèi)核通過 io復用函數(shù) 把其中的就緒事件通知給應(yīng)用程序
linux常用 io復用函數(shù): select poll epoll_wait; 本身是阻塞的, 具有同時監(jiān)聽多個io事件的能力
sigio 信號: 為一個 fd 指定 宿主進程 -> fd 上有事件發(fā)生 -> sigio 信號處理函數(shù)被觸發(fā) -> 被指定的宿主程序捕獲到 sigio信號

理論上 阻塞io / io復用 / 信號驅(qū)動io 都是 同步io模型: 先 io(就緒)事件, 后 io讀寫
異步io(aio.h): 用戶直接對io執(zhí)行讀寫操作 -> 內(nèi)核完成io操作 -> 通知應(yīng)用程序 io完成事件

同步 vs 異步: 內(nèi)核向應(yīng)用程序通知的時間(就緒事件 vs 完成時間) 由誰來完成io讀寫(應(yīng)用程序 vs 內(nèi)核)

2種高效事件處理模式

reactor模式(同步io): 主線程(io處理單元) 只負責監(jiān)聽 fd 上的事件, 有就通知 工作線程(邏輯單元), 不做其他實質(zhì)性工作; 工作線程完成 讀寫數(shù)據(jù)/接受連接/處理連接 等

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

proactor模式(異步io): 所有io操作交給主線程和內(nèi)核, 工作線程只負責業(yè)務(wù)邏輯; 更符合服務(wù)器編程框架

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

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

2種高效并發(fā)模式

并發(fā)編程: 如果是計算密集型, 并發(fā)編程沒有優(yōu)勢, 反而由于任務(wù)切換使效率降低; io密集型, io操作速度 遠小于 cpu計算速度

半同步/半異步模式: 同步 -> 程序完全按照代碼順序執(zhí)行, 異步 -> 程序執(zhí)行由系統(tǒng)事件(中斷/信號)來驅(qū)動; 同步 -> 邏輯單元, 異步 -> io單元
半同步/半反應(yīng)堆(half-sync/half-reactive)模式: 主線程(異步, 監(jiān)聽/連接 socket) -> 請求隊列 -> 工作線程(獲取連接socket); 缺點 -> 共享請求隊列, 需要加鎖 / 每個工作線程同一時間只能處理一個客戶請求, 增加工作進程會增加工作線程切換開銷
高效 半同步/半異步模式: 主線程 只監(jiān)聽socket -> 派發(fā)新請求給 工作進程 -> 工作進程 連接socket/處理io/維持自己的事件循環(huán)

領(lǐng)導者/追隨者模式: 多個工作現(xiàn)成輪流獲得事件源集合, 輪流監(jiān)聽/分發(fā)并處理事件; 1個領(lǐng)導者 + 多個追隨者(線程池, 休眠) -> 領(lǐng)導者監(jiān)聽到io事件 -> 自己處理io事件/線程池中選出新的領(lǐng)導者

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

有限狀態(tài)機 finite state machine

提供服務(wù)器性能的其他建議

池(pool): 服務(wù)器硬件資源相對 充裕 -> 空間換時間; 一組資源集合, 服務(wù)器啟動之初就完全創(chuàng)建并初始化, 這樣就成為了 靜態(tài)資源, 服務(wù)器正式運行時, 需要相關(guān)資源可以直接從池中獲取, 無須動態(tài)分配, 使用完后可以直接把資源放回池中, 無須執(zhí)行系統(tǒng)調(diào)用來釋放資源; 分配 足夠多 + 動態(tài)分配; 內(nèi)存池 / 進程池 / 線程池 / 連接池
內(nèi)存池: socket 接收緩存/發(fā)送緩存
進程池/線程池 -> 并發(fā)編程, 無須調(diào)用 fork/pthread_create
連接池: 服務(wù)器/服務(wù)器機群的內(nèi)部永久連接, 比如 db連接池

數(shù)據(jù)復制: 避免不必要的數(shù)據(jù)復制
內(nèi)存緩沖區(qū) -> 用戶程序緩沖區(qū): 內(nèi)核 直接處理, 比如 ftp服務(wù)器中使用 零拷貝 函數(shù) sendfile()
用戶代碼內(nèi) -> 比如2個進程間要傳遞大量數(shù)據(jù), 應(yīng)該考慮 共享內(nèi)存, 而不是 管道/消息隊列

上線文切換(context switch): 進程切換/線程切換 導致的 系統(tǒng)開銷
共享資源加鎖保護: 鎖 -> 不處理任何業(yè)務(wù)邏輯, 而且需要訪問內(nèi)核資源 -> 如果有更好的解決方案, 就應(yīng)該避免使用鎖 / 如果必須使用鎖, 應(yīng)盡量減少鎖的粒度

io復用

io復用: 程序同時監(jiān)聽多個 fd; 本身是阻塞的

使用 io 復用的場景:

  • client需要同時監(jiān)聽多個 socket
  • client需要同時處理用戶輸入和網(wǎng)絡(luò)連接
  • tcp server需要同時處理 socket 監(jiān)聽/連接 -> io復用最多的場合
  • server 需要同時處理 tcp/udp, 比如 回射 server
  • server 需要 監(jiān)聽多個端口/處理多個服務(wù), 比如 xinetd server

select 系統(tǒng)調(diào)用用途: 在一段指定時間內(nèi), 監(jiān)聽用戶感興趣的 fd 上的 read/write/exception 事件; fd 就緒條件: 可讀 -> balabala; 可寫 -> balabala; 處理帶外數(shù)據(jù)
poll 系統(tǒng)調(diào)用: 和 select 類似, 在一段時間內(nèi)輪詢一定數(shù)量的 fd, 以測試其中是否有就緒者

epoll 系統(tǒng)調(diào)用

使用一組函數(shù)來完成
把用戶關(guān)心的 fd 上的事件放在內(nèi)核的一個事件表中
LT(level trigger, 電平觸發(fā)): 默認, 相當于一個效率較高的 poll; epoll_wait 檢測到事件時就通知應(yīng)用程序, 應(yīng)用程序可以不立即處理(因為會重復通知)
ET(edge trigger, 邊沿觸發(fā)): epoll的高效工作模式; epoll_wait 檢測到事件時就通知應(yīng)用程序, 應(yīng)用程序必須立即處理(因為后序不會再通知), 減低了同一個 epoll 事件被重復觸發(fā)的次數(shù)
EPOLLONESHOT 事件: 一個 socket 連接在任一時刻都只被一個線程處理; 保證了連接完整性, 避免很多可能的競態(tài)條件

int epoll_create(int size)  # 創(chuàng)建額外的 fd, 用來標識內(nèi)核中的事件表
int epoll_ctl()             # 操作內(nèi)核事件表
int epoll_wait()            # 在一段超時時間內(nèi)等待一組 fd 上的事件 -> 只傳遞就緒事件, 不處理用戶注冊的事件

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

信號

用戶/系統(tǒng)/進程 發(fā)送給目標進程的信息, 以通知目標進程 某個狀態(tài)的改變/系統(tǒng)異常
被掛起的信號: 設(shè)置進程信號掩碼 -> 屏蔽信號(程序不用處理所有的信號)
統(tǒng)一事件源: 信號事件/io事件一樣被處理; 信號事件 -> 管道 -> 監(jiān)聽管道讀端fd 上的可讀事件 -> io事件; 如 libevent 庫

linux信號可由如下條件產(chǎn)生:

  • 對于前臺進程, 可以通過輸入特殊的終端字符來發(fā)送信號, 如 Ctrl-C 通常會發(fā)送一個中斷信號
  • 系統(tǒng)異常, 如 浮點異常/非法內(nèi)存段訪問
  • 系統(tǒng)狀態(tài)變化, 如 alarm定時器到期 -> SIGALRM 信號
  • 運行kill命令/調(diào)用kill函數(shù)
int kill(pid_t pid, int sig)            # 發(fā)送信號; pid -> pid/本進程組/除init進程外/其他進程組; sig -> 都大于0
int signal()                            # 為一個信號設(shè)置處理函數(shù)
int sigaction()                         # 更健壯的接口
int sigpending()                        # 獲得當前進程被掛起的信號

網(wǎng)絡(luò)編程相關(guān)信號:

  • SIGHUP: 掛起進程的控制終端; 沒有控制終端的網(wǎng)絡(luò)后臺程序 -> 強制服務(wù)器重讀配置文件
  • SIGPIPE: 向 讀端關(guān)閉的 管道/socket 寫數(shù)據(jù) -> 默認關(guān)閉進程 -> 不希望錯誤的寫操作而導致程序退出 -> 代碼中捕獲并處理該信號/至少忽略
  • SIGURG: 內(nèi)核通知應(yīng)用程序帶外數(shù)據(jù)到達 -> io復用/SIGURG信號

定時器

需要處理的第三類事件 - 定時事件: 如 定時檢測一個客戶連接的活動狀態(tài); 有效組織, 預期觸發(fā) + 不影響主要邏輯
定時事件 -> 封裝成 定時器 -> 使用某種容器類數(shù)據(jù)結(jié)構(gòu)(2種高效管理定時器的容器: 時間輪/時間堆) -> 將所有定時器串聯(lián)起來 -> 實現(xiàn)對定時事件的統(tǒng)一管理
linux 3種定時方法: socket選項 SO_RECVTIEMO/SO_SENDTIEMO; SIGALRM 信號; io復用超時參數(shù)
定時器至少包含2個成員: 超時時間(絕對/相對) + 任務(wù)回調(diào)函數(shù)

定時tick: 使用固定頻率心搏函數(shù)tick -> 依次檢測到期定時器 -> 執(zhí)行定時器上的回調(diào)函數(shù)(時間輪)
最短時間tick: 每次使用最小定時器超時值作為tick -> tick調(diào)用 -> 最小定時器被調(diào)用 -> 更新剩余定時器(時間堆)

簡單時間輪: 每個槽(slot)用有相同的槽間隔si(slot interval, 其實就是心搏時間tick); 每個槽放在一個 定時器鏈表, 新的定時器分配通過定時時間hash到不同的槽
復雜時間輪 -> 類似 水表, 有不同精度的輪子
時間堆: 最小堆實現(xiàn)

高性能io框架庫 - Libevent

linux服務(wù)器必須處理的3類事件: io事件/信號/定時事件
處理事件需要考慮的問題: 統(tǒng)一事件源 -> io復用; 可移植性; 對并發(fā)編程的支持, 避免競態(tài)條件

句柄(handle): 統(tǒng)一事件源 -> 綁定句柄 -> 內(nèi)核檢測到就緒事件 -> 通過句柄通知應(yīng)用程序
事件循環(huán): 無法預知客戶 連接請求/暫停信號 -> 循環(huán)等待并處理
事件多路分發(fā)器(EventDemultiplexer): 封裝各種 io復用系統(tǒng) 為統(tǒng)一的接口
具體事件處理器: 框架提供一個接口, 應(yīng)用程序來自己擴展

Libevent 源碼分析:

  • 跨平臺
  • 統(tǒng)一事件源
  • 線程安全, 使用 libevent_pthreads
  • 基于 Reactor 模式實現(xiàn)
  • 編寫產(chǎn)品級函數(shù)庫需要考慮哪些細節(jié)
  • 提高C語言功底: 大量函數(shù)指針 + 多態(tài)機制 + 一些基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)的高效實現(xiàn)

多進程編程

多進程編程內(nèi)容:

  • fork系統(tǒng)調(diào)用 -> 復制進程映像; exec系統(tǒng)調(diào)用 -> 替換進程映像
  • 僵尸進程以及如何避免
  • 進程間通信(inter-process communication, IPC)最簡單的方式: 管道
  • 3種System V IPC: 信號量/消息隊列/共享內(nèi)存
  • 進程間傳遞fd的通用做法: 通過unix本地域socket傳遞特殊的輔助數(shù)據(jù)

fork 系統(tǒng)調(diào)用: 創(chuàng)建進程; 每次調(diào)用返回2次 -> 父進程返回子進程pid, 子進程返回0; 寫時復制(copy on write)
exec 系統(tǒng)調(diào)用: 子進程中執(zhí)行其他程序; 原程序已經(jīng)被exec參數(shù)指定的程序完全替換(代碼+數(shù)據(jù)) -> 原程序在exec調(diào)用之后的代碼都不會執(zhí)行

僵尸態(tài) 1: 父進程一般需要跟蹤子進程退出狀態(tài), 所以子進程退出時內(nèi)核不會立即釋放該進程進程表表項 -> 子進程退出之后, 父進程讀取其退出狀態(tài)之前
僵尸態(tài) 2: 父進程異常 結(jié)束/異常終止, 子進程繼續(xù)運行, os將其ppid設(shè)置為1(init進程) -> 父進程退出之后, 子進程退出之前

訪問共享資源的代碼 -> 關(guān)鍵代碼段/臨界區(qū)
進程同步問題 -> 同一個時刻只有一個進程可以擁有對資源的獨占式訪問 -> 確保任一時刻只有一個進程能進入關(guān)鍵代碼段
信號量(semaphore): 特殊的變量, 只能取自然數(shù)值并只支持2種操作 P/V 操作

共享內(nèi)存: 最高效IPC機制; 需要輔助手段同步進程對共享內(nèi)存的訪問; 通常和其他IPC方式一起使用;
api1: sys/shm.h -> shmget/shmat/shmdt/shmctl
api2: mmap() + 打開同一個文件 -> 無關(guān)進程之間共享內(nèi)存
實例: 聊天室服務(wù)器

管道/命名管道: 必須 FIFO 方式接收數(shù)據(jù)
消息隊列: 在2個進程之間傳遞二進制數(shù)據(jù)塊的一種簡單有效方式; 每個數(shù)據(jù)塊包含特定type -> 接收方有選擇的接收數(shù)據(jù)
api: sys/msg.h -> msgget() / msgsnd / msgrcv / msgctl

傳遞fd: 接收進程創(chuàng)建一個新的fd -> 發(fā)送進程和新進程的 fd 都執(zhí)行內(nèi)核中形同的文件表項

pid_t fork(void)

# 退出進程, 避免僵尸進程
wait()                  # 將進程阻塞, 直到某個子進程結(jié)束運行
waitpid()               # 只等待pid參數(shù)指定的子進程

# 管道
pipe()                  # 創(chuàng)建管道
socketpair()            # 創(chuàng)建全雙工管道

# 信號量 P/V操作
P(sv): sv>0 -> --sv; sv==0 -> 掛起
V(sv): 其他進程等待 sv -> 喚醒; 沒有, ++sv

# 信號量系統(tǒng)調(diào)用
int semget()            # 創(chuàng)建新的信號量集
int semop()             # 改變信號量, 即 P/V 操作
int semctl()            # 允許調(diào)用者對信號量進行直接控制

# 共享內(nèi)存系統(tǒng)調(diào)用
int shmget()            # 創(chuàng)建/獲取 共享內(nèi)存
void shmat()            # 關(guān)聯(lián) 共享內(nèi)存 到進程的地址空間
int shmdt()             # 從進程地址空間 分離 共享內(nèi)存
int shmctl()            # 控制 共享內(nèi)存 某些屬性

# ipc 相關(guān)命令
ipcs                    # 列出os中共享資源
ipcrm                   # 刪除遺留在os中的共享資源

多線程編程

linux線程庫 -> NPTL(native POSIX thread library)

POSIX 線程(簡稱 pthread)標準:

  • 創(chuàng)建/結(jié)束 線程
  • 讀取/設(shè)置 線程屬性
  • 同步方式: POSIX信號量 / 互斥鎖 / 條件變量

根據(jù)運行環(huán)境和調(diào)度者: 內(nèi)核線程 -> 運行在內(nèi)核空間, 由內(nèi)核來調(diào)度; 用戶線程 -> 運行在用戶空間, 由線程庫來調(diào)度; 內(nèi)核線程 作為 用戶線程 運行的容器
完全在用戶空間實現(xiàn)的線程; 無須內(nèi)核支持; 對應(yīng)一個內(nèi)核線程; 優(yōu)點 -> 創(chuàng)建/調(diào)度 快, 不占用額外系統(tǒng)資源; 缺點 -> 一個進程的多個線程無法運行在不同 cpu 上
完全由內(nèi)核調(diào)度: 1:1 映射用戶空間線程和內(nèi)核線程
雙層調(diào)度: 混合上面2種

pthread api: pthread.h

POSIX 信號量: 和IPC中的信號量定義一樣; api -> semaphore.h
互斥鎖: 同步線程對共享數(shù)據(jù)的訪問; 用來保護關(guān)鍵代碼塊(加鎖/解鎖), 類似二進制信號量; api -> pthread.h -> pthread_mutex_*
互斥鎖死鎖: 對一個已經(jīng)加鎖的普通鎖再次加鎖 -> 如設(shè)計不夠仔細的遞歸函數(shù); 2個線程按照不同的順序唉申請2個互斥鎖
條件變量: 線程之間同步共享數(shù)據(jù)的值; api -> pthread.h -> pthread_cond_*

可重入函數(shù): 能被多個線程同時調(diào)用且不發(fā)生靜態(tài)條件 -> 線程安全(thread safe); linux庫函數(shù)只有一小部分不可重入; 不可重入函數(shù)的重入版本 -> 函數(shù)末尾加 _r

# pthread
pthread_read()              # 創(chuàng)建
pthread_exit()              # 最好調(diào)用此函數(shù), 以確保安全/干凈地退出
pthread_join()              # 同一個進程的所有進程都可以調(diào)用此函數(shù)來回收其他線程
pthread_cancel()            # 異常終止一個線程

pthread_atfork()            # 確保fork調(diào)用后父進程和子進程都擁有一個清楚的鎖狀態(tài)
pthread_sigmask()           # 每個線程可以獨立的設(shè)置信號掩碼

進程池和線程池

動態(tài)創(chuàng)建缺點: 耗時, 響應(yīng)慢; 動態(tài)創(chuàng)建 子進程/子線程 為一個客戶服務(wù)會產(chǎn)生大量細微 進程/線程, 進程/線程 切換將消耗大量cpu; 動態(tài)創(chuàng)建 子進程 是當前進程的完整映像, 必須警慎管理分配的 fd 等系統(tǒng)資源

進程池: 建立主進程 -> 主進程建立主進程 -> 新任務(wù) -> 分配任務(wù) -> 通知機制(主進程傳遞信息給子進程)
分配任務(wù)方式: 主動選擇 -> 隨機算法/輪流選取(round robin) -> 均衡分配; 工作隊列
通知機制: 預先建立管道; 父子線程之間只需要把這些數(shù)據(jù)定義為全局即可
處理多用戶: 并發(fā)模式選擇; 常連接(一個客戶多次請求可以復用一個tcp連接); 同一個客戶是否由同一個進程來處理

高性能服務(wù)器優(yōu)化與監(jiān)控

服務(wù)器 調(diào)制/調(diào)試/測試

系統(tǒng)配置調(diào)制

fd: 幾乎所有的系統(tǒng)調(diào)用都是和 fd 打交道, 而系統(tǒng)的分配的 fd數(shù)量 是有限制的, 所以我們總是要關(guān)閉那些不在使用的 fd, 以釋放占用的資源, 如 守護進程關(guān)閉 stdio/stderr
最大 fd 限制: 用戶級 + 系統(tǒng)級
內(nèi)核模塊

ulimit -n                               # 查看用戶級 fd 限制
ulimit -SHn mak-file-number             # (臨時)修改用戶級 fd 限制為 max-file-number
/etc/security/limits.conf               # (永久)
sysctl -w fs.file-max=max-file-number   # (臨時)修改系統(tǒng)級 fd 限制為 max-file-number
/etc/sysctl.conf                        # (永久)

sysctl -a                               # 查看下面這些內(nèi)核參數(shù)
/proc/sys                               # 幾乎所有的 內(nèi)核模塊+驅(qū)動程序 都在此文件系統(tǒng)下提供了某些配置文件以供用戶調(diào)整模塊的屬性和行為
    fs/                                 # 文件系統(tǒng)相關(guān)
        file-max                        # 系統(tǒng)級 fd 限制(臨時修改)
        innode-max                      # 應(yīng)當設(shè)置為 file-max 的 3-4 倍
        epoll/max_user_wathes           # 一個用戶能忘epoll內(nèi)核事件表注冊的事件總量
    net/                                # 網(wǎng)絡(luò)模塊
        core/
            somaxconn                   # listen監(jiān)聽隊列: ESTABLISH最大數(shù)量
        ipv4/
            tcp_max_syn_backlog         # listen監(jiān)聽隊列: ESTABLISH+SYN_RCVD 最大數(shù)量
            tcp_wmem                    # 一個socket的tcp寫緩沖區(qū) min/default/max
            tcp_rmem                    # 一個socket的tcp讀緩沖區(qū) min/default/max -> 接收通告窗口
            tcp_syncookies              # 是否打開tcp同步標簽(tcp_syncookies)
        ipv6/

gdb調(diào)試

# 調(diào)試子進程
ps -ef|grep cgisrv
gdb
(gdb) gdb attach 4183 # 附加子進程
(gdb) b processpool.h:264 # 設(shè)置子進程中的斷點

(gdb) set follow-fork-mode parent/child # 程序執(zhí)行 fork 系統(tǒng)調(diào)用后調(diào)試 父/子 進程

# 調(diào)試多線程程序
info threads # 顯示當前可調(diào)試的所有線程, 線程會帶上id
thread id # 調(diào)試指定id的線程
set scheduler-locking off/on/step # 是否只讓當前調(diào)試進程運行

壓力測試

思路: 使用 epool 實現(xiàn)io復用來模仿壓力

系統(tǒng)監(jiān)控工具

tcpdump: tcp 抓包工具
lsof(list open file): 查看打開的 fd
nc(netcat): 快速構(gòu)建網(wǎng)絡(luò)連接
strace: 測試服務(wù)器性能的重要工具
netstat: 網(wǎng)絡(luò)信息統(tǒng)計
vmstat(virtual memory statistic): 實時輸出系統(tǒng)各種資源的使用情況
ifstat(interface statistic): 簡單的網(wǎng)絡(luò)流量監(jiān)測工具
mpstat(multi-processor statistic): 實時監(jiān)測多處理器系統(tǒng)上的每個 cpu 使用情況

# 使用表達式進一步過濾數(shù)據(jù)
tcpdump net 1.2.3.0/24 # type: host net port portrange
tcpdump dst port 13679 # dir(方向): src dst
tcpdump icmp # proto(協(xié)議): 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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 必備的理論基礎(chǔ) 1.操作系統(tǒng)作用: 隱藏丑陋復雜的硬件接口,提供良好的抽象接口。 管理調(diào)度進程,并將多個進程對硬件...
    drfung閱讀 3,746評論 0 5
  • Java繼承關(guān)系初始化順序 父類的靜態(tài)變量-->父類的靜態(tài)代碼塊-->子類的靜態(tài)變量-->子類的靜態(tài)代碼快-->父...
    第六象限閱讀 2,246評論 0 9
  • 一. 操作系統(tǒng)概念 操作系統(tǒng)位于底層硬件與應(yīng)用軟件之間的一層.工作方式: 向下管理硬件,向上提供接口.操作系統(tǒng)進行...
    月亮是我踢彎得閱讀 6,144評論 3 28
  • 體現(xiàn)價值的方法 故事:卓別林開始拍電影時,那些導演們都堅持讓他模仿當時一位非常有名的德國喜劇演員。卓別林卻一直保持...
    Tracy_zhang閱讀 120評論 0 0
  • 孩子們的冒險夢永遠是動畫作品不可缺失的主題,哆啦A夢系列更是這樣。 但是不代表這是一個隨便可以寫好的主題,上一部《...
    紙迷心竅閱讀 560評論 0 0

友情鏈接更多精彩內(nèi)容