Linux 高性能服務(wù)器編程
原文地址以及最新代碼參考:https://github.com/EricPengShuai/Interview/tree/main/Linux
Ch.5 Linux 網(wǎng)絡(luò)編程基礎(chǔ) API
5.1 socket 地址 API
5.1.1 主機(jī)字節(jié)序和網(wǎng)絡(luò)字節(jié)序
- 大端字節(jié)序(網(wǎng)絡(luò)字節(jié)序):高位低地址
- 小端字節(jié)序(主機(jī)字節(jié)序):高位高地址
union {
short value;
char union_bytes[sizeof(short)];
} test;
test.value = 0x0102;
test.union_bytes[0] == 1 && test.union_bytes[1] == 2; // 大端
test.union_bytes[0] == 2 && test.union_bytes[1] == 1; // 小端
參考代碼:5-1byteorder.cpp
一般網(wǎng)絡(luò)編程中,發(fā)送端會將發(fā)送的數(shù)據(jù)轉(zhuǎn)換成大端字節(jié)序數(shù)據(jù)后再發(fā)送,接收端這邊根據(jù)自身采用的字節(jié)序決定是否對接受的數(shù)據(jù)進(jìn)行轉(zhuǎn)換(小端機(jī)轉(zhuǎn)換,大端機(jī)不轉(zhuǎn)),Linux 提供一下函數(shù):
// h: host; n: network; l: long; s: short
// long: 往往用來轉(zhuǎn)換 IP; short: 往往用來轉(zhuǎn)換 port
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned long int netshort);
5.1.2 通用 socket 地址
sockaddr 結(jié)構(gòu)體表示 socket 地址
#include <bits/socket.h> // <sys/scoket.h>
struct sockaddr {
sa_family_t sa_family; // 常見的地址簇有: AF_UNIX/AF_INET/AF_INET6
char sa_data[14]; // 存放 socket 地址值,不同的地址簇地址值長度不同
}
地址簇(address family)和協(xié)議簇(protocol family)一一對應(yīng),兩者是一樣的,經(jīng)?;煊?/p>
- AF_*: AF_UNIX / AF_INET / AF_INET6
- PF_*: PF_UNIX / PF_INET / PF_INET6
UNIX 本地域協(xié)議簇和 TCP/IPv6 協(xié)議簇的地址值長度遠(yuǎn)遠(yuǎn)超過 14 字節(jié),因?yàn)橐肓艘粋€(gè)新的通用 socket 地址結(jié)構(gòu)體 sockaddr_storage,具體參考 P72
5.1.3 專用 socket 地址
上面的 sa_data 將 IP 地址和端口號混在一起,實(shí)際使用中經(jīng)常使用 sockaddr_in 和 sockaddr_in6 結(jié)構(gòu)體,分別針對于 AF_INET 和 AF_INET6 地址簇
但是所有使用專用 socket 地址以及 sockaddr_storage 類型的變量在實(shí)際中又需要強(qiáng)制轉(zhuǎn)換成通用的 socket 地址類型 sockaddr,因?yàn)樗械?socket 編程結(jié)構(gòu)的地址參數(shù)類型都是 sockaddr
5.1.4 IP 地址轉(zhuǎn)換函數(shù)
Linux 提供三個(gè)「點(diǎn)分十進(jìn)制字符串表示的 IPv4 地址和用網(wǎng)絡(luò)字節(jié)序整數(shù)表示的 IPv4 地址之間轉(zhuǎn)換」的接口
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr); // 失敗返回 INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp); // 失敗返回 0,成功返回 1
char* inet_ntoa(struct in_addr in); // 逆向轉(zhuǎn)換,注意該函數(shù)的不可重入性,具體參考 P73
更適用與 IPv4/IPv6 的函數(shù)為:
#include <arpa/inet.h>
int inet_pton(int af, const char* src, void* dst); // af 是地址簇
// 逆向轉(zhuǎn)換,len 可以為 INET_ADDRSTRLEN(16, IPv4) / INET6_ADDRSTRLEN(46, IPv6)
const char* inet_ntop(int af, const void* src, char* dst, socklen_t len); // 失敗返回 NULL
5.2 創(chuàng)建 socket
UNIX/Linux 一切皆文件的思想也囊括 socket,它是一個(gè)可讀、可寫、可控制、可關(guān)閉的文件描述符,創(chuàng)建的一個(gè) socket 的系統(tǒng)調(diào)用為:
#include <sys/types.h>
#include <sys/socket.h>
// domain: 協(xié)議簇,包括 PF_INET、PF_INET6、PF_UNIX
// type: 服務(wù)類型,包括 SOCK_STREAM(TCP流服務(wù))、SOCK_DGRAM(UDP數(shù)據(jù)報(bào)服務(wù))
// protocol: 前兩個(gè)參數(shù)已經(jīng)決定了協(xié)議,一般設(shè)置為 0 即可
int socket(int domain, int type, int protocol); // 失敗返回 -1
type 在 Linux 2.6.17 版本可以接受 SOCK_NONBLOCK 和 SOCK_CLOEXEC 宏,具體含義參考 P75
5.3 命名 socket
將 socket 與具體的 IP 地址綁定稱為命名 socket,服務(wù)端程序中只有命名 socket 之后客戶端才能知道該如何連接它,客戶端通常不需要命名 socket,而是采用匿名方式。
#include <sys/types.h>
#include <sys/socket.h>
// 將 my_addr 所指的 socket 地址分配給 未命名的 sockfd 文件描述符
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen); // 失敗返回 -1
5.4 監(jiān)聽 socket
socket 被命名之后還需要創(chuàng)建監(jiān)聽隊(duì)列以存放待處理的客戶端連接,才能接受客戶端連接
#include <sys/socket.h>
// backlog 是內(nèi)核監(jiān)聽隊(duì)列的最大長度,表示服務(wù)端完全連接狀態(tài) ESTABLISHED 數(shù)量的上限(backlog+1)
// Mac 環(huán)境中測試是監(jiān)聽上限就是 backlog
int listen(int sockfd, int backlog); // 失敗返回 -1,成功返回 0
代碼參考:5-3testlisten.cpp
5.5 接受連接
從 listen 監(jiān)聽隊(duì)列中接受一個(gè)連接的系統(tǒng)調(diào)用為:
#include <sys/types.h>
#include <sys/socket.h>
// sockfd 是執(zhí)行過 listen 系統(tǒng)調(diào)用的監(jiān)聽 socket,處于 LISTEN 狀態(tài)
// addr 用來獲取被接受連接的遠(yuǎn)端 socket 地址
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
accept 成功調(diào)用會返回一個(gè)新的連接 socket(處于 ESTABLISHED 狀態(tài)),該套接字唯一標(biāo)識這個(gè)被接受的連接,服務(wù)器可以通過讀寫該 socket 來與客戶端通信
注意:accept 并不關(guān)心任何網(wǎng)絡(luò)狀況的變化,只是從監(jiān)聽隊(duì)列中取出連接,而不論連接處于何種狀態(tài)
代碼參考:5-5testaccept.cpp
5.6 發(fā)起連接
服務(wù)端是通過 listen 被動接受連接,客戶端就需要通過 connect 系統(tǒng)調(diào)用主動發(fā)起與服務(wù)端的連接:
#include <sys/types.h>
#include <sys/socket.h>
// sockfd 是客戶端自己創(chuàng)建的套接字
// serv_addr 是服務(wù)器監(jiān)聽的 socket 地址
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
成功調(diào)用返回 0,客戶端的 sockfd 就唯一標(biāo)識這個(gè)連接,客戶端就可以通過讀寫 sockfd 來與服務(wù)器通信
5.7 關(guān)閉連接
通過減少引用計(jì)數(shù)關(guān)閉連接,并不是真正關(guān)閉連接,只有當(dāng)引用計(jì)數(shù)為 0 時(shí)才真正關(guān)閉
#include <unistd.h>
int close(int fd);
立即關(guān)閉連接可以使用:
#include <sys/socket.h>
// howto: 關(guān)閉讀 SHUT_RD、關(guān)閉寫 SHUT_WR、關(guān)閉讀寫 SHUT_RDWR
int shutdown(int sockfd, int howto); // 成功返回 0,失敗 -1
5.8 數(shù)據(jù)讀寫
5.8.1 TCP 數(shù)據(jù)讀寫
#include <sys/types.h>
#include <sys/socket.h>
// buf 和 len 分別是讀緩沖區(qū)的位置和大小,flags 一般為 0
// recv 成功時(shí)返回實(shí)際讀取的數(shù)據(jù)長度,可能小于 len
// 返回 0 表示對方已經(jīng)關(guān)閉連接,-1 表示出錯(cuò)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// send 成功時(shí)返回實(shí)際寫入的數(shù)據(jù)長度,失敗時(shí)返回 -1
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
flags 選項(xiàng)可以設(shè)置 send 或者 recv 讀取或者發(fā)送數(shù)據(jù)的方式,比如處理緊急數(shù)據(jù)(帶外數(shù)據(jù))等
帶外數(shù)據(jù)處理代碼參考:客戶端-5-6oobsend.cpp,服務(wù)端-5-7oobrecv.cpp
5.8.2 UDP 數(shù)據(jù)讀寫
#include <sys/types.h>
#include <sys/socket.h>
// 和 recv 不同的是需要指定發(fā)送端的 socket 地址,因?yàn)?UDP 沒有連接的概念
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
// 指定接收端的 socket 地址
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, struct sockaddr* dest_addr, socklen_t* addrlen);
這兩個(gè)函數(shù)也可以用于 TCP 接受數(shù)據(jù),只需要將后面兩個(gè)參數(shù)設(shè)置為 NULL 表示忽略發(fā)送端/接收端地址
5.8.3 通用數(shù)據(jù)讀寫函數(shù)
不區(qū)分 TCP 和 UDP,更加通用的數(shù)據(jù)讀寫函數(shù)為:
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
sszie_t sendmsg(int sockfd, struct msghdr* msg, int flags);
msghdr 結(jié)構(gòu)體為:具體含義參考 P85
struct msghdr {
void* msg_name; // socket 地址
socklen_t msg_namelen; // 地址長度
struct iovec* msg_iov; // 存放數(shù)據(jù):分散讀、集中寫
int msg_iovlen; // iovec 結(jié)構(gòu)體個(gè)數(shù)
void* msg_control;
socklen_t msg_controllen;
int msg_flags; // 復(fù)制函數(shù)中 flags 參數(shù),調(diào)用過程中更新
};
5.9 帶外標(biāo)記
內(nèi)核通知應(yīng)用程序帶外數(shù)據(jù)到達(dá)的兩種方式:「IO復(fù)用產(chǎn)生的異常事件」和 「SIGURG 信號」
#include <sys/socket.h>
// 判斷 sockfd 是否處于帶外標(biāo)記,即下一個(gè)被讀到的數(shù)據(jù)是否是帶外數(shù)據(jù)
// 如果是返回 1,此時(shí)就可以設(shè)置 flags 為 MSG_OOB 標(biāo)志的 recv 調(diào)用來接受帶外數(shù)據(jù);否則返回 0
int sockatmark(int socket);
5.10 地址信息函數(shù)
#include <sys/socket.h>
// 獲取 sockfd 對應(yīng)的本端 socket 地址,存在 addr 中
int getsockname(int sockfd, struct sockaddr* addr, socklen_t* addlen); // 成功返回 0,失敗 -1
// 獲取 sockfd 對應(yīng)的遠(yuǎn)端 socket 地址,存在 addr 中
int getpeername(int sockfd, struct sockaddr* addr, socklen_t* addlen); // 成功返回 0,失敗 -1
5.11 socket 選項(xiàng)
#include <sys/socket.h>
// level: 指定哪個(gè)協(xié)議,包括 SOL_SOCKET、IPPROTO_IP、IPPROTO_IPV6、IPPROTO_TCP
// option_name: 指定選項(xiàng)名稱
// option_value 和 option_len 分別是選項(xiàng)的值和長度
int getsockopt(int sockfd, int level, int option_name, void* option_value,
socketlen_t* restrict option_len); // 成功返回 0,失敗 -1
int setsockopt(int sockfd, int level, int option_name, const void* option_value,
socketlen_t option_len); // 成功返回 0,失敗 -1
-
SO_REUSEADDR:強(qiáng)制使用被處于 TIME_WAIT 狀態(tài)的連接占用的 socket 地址
代碼參考:5-9reuse_address.cpp
-
SO_RCVBUF:修改 TCP 接受緩沖區(qū)的大小(翻倍設(shè)置),最小為 256 字節(jié)
代碼參考:5-11set_recv_buffer.cpp
-
SO_SNDBUF:修改 TCP 發(fā)送緩沖區(qū)的大小(翻倍設(shè)置),最小為 2048 字節(jié)
代碼參考:5-10set_send_buffer.cpp
-
SO_RCVLOWAT 和 SO_SNDLOWAT:TCP 接受緩沖區(qū)和發(fā)送緩沖區(qū)的低水位標(biāo)記,用于 IO 復(fù)用中判斷何時(shí)可讀寫
5.12 網(wǎng)絡(luò)信息 API
#include <netdb.h>
// 根據(jù)主機(jī)名稱獲取主機(jī)的完整信息
struct hostent* gethostbyname(const char* name);
// 根據(jù) IP 地址獲取主機(jī)的完整信息
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
返回的都是 hostent 結(jié)構(gòu)體類型的指針,更多具體的 API 參考 P95
代碼參考:5-12access_daytime.cpp
Ch.6 高級 I/O 函數(shù)
Linux 提供很多高級 IO 函數(shù),沒有 read/open 等基礎(chǔ)的常用,但是特定地方使用性能較高,一般有三類
- 創(chuàng)建文件描述符的函數(shù):pipe、dup/dup2
- 讀寫數(shù)據(jù)的函數(shù):包括 readv/writev、sendfile、mmap/munmap、splice、tee 等
- 控制 IO 行為和屬性的函數(shù):fcntl
6.1 pipe 函數(shù)
#include <unistd.h>
// 往 fd[1] 寫入的數(shù)據(jù)可以從 fd[0] 讀出,不能反過來
int pipe(int fd[2]); // 成功返回 0,并將一對打開的文件描述符填入其參數(shù)指向的數(shù)組
- 管道內(nèi)部傳輸?shù)臄?shù)據(jù)是字節(jié)流
- 管道有容量限制,默認(rèn)大小為 65536 字節(jié)
- socketpair 函數(shù)可以在本地域 AF_UNIX 創(chuàng)建雙向管道
6.2 dup/dup2 函數(shù)
#include <unistd.h>
// 返回一個(gè)新的文件描述符,和舊的文件描述符 old_fd 指向相同的文件、管道或者網(wǎng)絡(luò)連接
// 返回系統(tǒng)當(dāng)前可用的最小整數(shù)值
int dup(int old_fd);
// 和 dup 類似,返回的文件描述符不小于 limit_fd
int dup2(int old_fd, int limit_fd);
P102 例子中,寫了一個(gè)簡單的服務(wù)端程序,與客戶端通信的 socket 記為 connfd,先關(guān)閉標(biāo)準(zhǔn)輸出 STDOUT_FILENO (其值為1),然后調(diào)用 dup(connfd) 返回 1,這樣標(biāo)準(zhǔn)輸出就和 connfd 指向同樣的文件,也就是 printf 的數(shù)據(jù)直接寫入管道(不會出現(xiàn)在終端上),發(fā)送給客戶端,這就是 Comman Gateway Interface(CGI)服務(wù)器的基本工作原理
代碼參考:6-1testdup.cpp
6.3 readv/writev 函數(shù)
簡單來說 readv 是分散讀,writev 是集中寫,相當(dāng)于 recvmsg/sendmsg
#include <sys/uio.h>
// fd 是被操作的 socket,vector 是 iovec 結(jié)構(gòu)數(shù)組,iovec 結(jié)構(gòu)描述的是一塊內(nèi)存區(qū),count 參數(shù)是 vector 數(shù)組長度
// 成功時(shí)返回讀出/寫入 fd 的字節(jié)數(shù),失敗返回 -1 并設(shè)置 errno
ssize_t readv(int fd, const struct iovec* vector, int cnt);
ssize_t writev(int fd, const struct iovec* vector, int cnt);
strcut iovec {
void *iov_base; // 內(nèi)存起始地址
size_t iov_len; // 內(nèi)存長度
};
P105 例子中給了簡單 HTTP 文件服務(wù)器,通過 writev 將 headbuf(狀態(tài)行+頭部字段+空行)和 filebuf(文檔內(nèi)容)集中寫入 socket
代碼參考:6-2testwritev.cpp
6.4 sendfile 函數(shù)
sendfile 在兩個(gè)文件描述符之間直接傳遞數(shù)據(jù),完全在內(nèi)核中操作,避免了內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的數(shù)據(jù)拷貝,這就是零拷貝
#include <sys/sendfile.h>
// in_fd --sendfile--> out_fd
// in_fd 表示待讀出內(nèi)容的文件描述符,out_fd 表示待寫入內(nèi)容的文件描述符
// offset 表示 in_fd 的起始位置,count 表示 in_fd 和 out_fd 之間傳輸?shù)淖止?jié)數(shù)
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count); // 成功時(shí)返回傳輸?shù)淖止?jié)數(shù),失敗返回-1
- in_fd 必須是一個(gè)支持類似 mmap 函數(shù)的文件描述符,必須指向真實(shí)的文件,不能是 socket 和管道
- out_fd 必須是一個(gè) socket
sendfile 幾乎是專門為在網(wǎng)絡(luò)上傳輸文件而設(shè)計(jì)的,注意 MacOS 的 sendfile 參數(shù)和 Linux 不太一樣,參考 sendfile.2
P107 例子使用 sendfile 將服務(wù)器上的一個(gè)文件傳輸給客戶端,其中沒有為目標(biāo)文件分配任何用戶空間的緩存,也沒有執(zhí)行讀取文件的操作,相比于之前的 通過 HTTP 傳輸文件的效率要高得多
代碼參考:6-3testsendfile.cpp
6.5 mmap/munmap 函數(shù)
mmap 用于申請一段內(nèi)存空間,munmap 則釋放由 mmap 創(chuàng)建的這段內(nèi)存空間
#include <sys/mman.h>
// start: 待分配內(nèi)存的起始地址,如果為 null 則系統(tǒng)自動分配一個(gè)地址
// length: 指定內(nèi)存段的長度;prot: 設(shè)置內(nèi)存段的訪問權(quán)限,可以按位或取 PROT_READ|PROT_WRITE|PROT_EXEC|PROT_NONE
// fd 是被映射文件對應(yīng)的文件描述符,一般通過 open 獲得
// [return] 成功時(shí)返回指向目標(biāo)內(nèi)存區(qū)域的指針,失敗 -1
void* mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length); // 失敗 -1,成功 0
6.6 splice 函數(shù)
splice 函數(shù)用于兩個(gè)文件描述符之間移動數(shù)據(jù),是零拷貝操作
#include <fcntl.h>
// fd_in 表示待輸入數(shù)據(jù)的文件描述符
// len 指定移動數(shù)據(jù)的長度
// flags 控制數(shù)據(jù)如何移動,取異或值:SPLICE_F_MOVE|SPLICE_F_NONBLOCK|SPLICE_F_MORE
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags); // 成功時(shí)返回移動字節(jié)的數(shù)量,失敗 -1
fd_in/fd_out 必須至少有一個(gè)是管道文件描述符
- fd_in 如果是管道文件描述符,off_in 參數(shù)必須被設(shè)置為 NULL
- fd_in 如果不是一個(gè)管道文件描述符,off_in 指定輸入數(shù)據(jù)流的何處開始讀取數(shù)據(jù)
P110 例子實(shí)現(xiàn)了一個(gè)簡單的回射服務(wù)器,利用 splice 函數(shù)將客戶端的內(nèi)容讀入管道寫端 fd[1],然后再使用 splice 函數(shù)從管道讀端 fd[0] 讀出該內(nèi)容到客戶端。整個(gè)過程沒有執(zhí)行 recv/send 操作,十分高效
代碼參考:6-4testsplice.cpp (注:MacOS 并沒有 splice 函數(shù))
6.7 tee 函數(shù)
tee 函數(shù)在兩個(gè)管道文件描述符之間復(fù)制數(shù)據(jù),是零拷貝操作
#include <fcntl.h>
// fd_in 和 fd_out 必須都是管道文件描述符
// [return] 成功時(shí)返回復(fù)制的字節(jié)數(shù),失敗 -1
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
P111 例子實(shí)現(xiàn)了一個(gè)簡單的 tee 程序,利用 splice(標(biāo)準(zhǔn)輸入輸出<–>輸入輸出管道) 和 tee(輸出管道<–>文件管道)同時(shí)輸出數(shù)據(jù)到終端和文件
代碼參考:6-5testtee.cpp
6.8 fcntl 函數(shù)
file control 函數(shù)提供對文件描述符的各種控制操作
#include <fcntl.h>
// fd 是被操作的文件描述符,cmd 指定執(zhí)行何種類型的操作
// 根據(jù)操作類型不同可能還需要第3個(gè)可選參數(shù) arg
int fcntl(int fd, int cmd, ...); // 失敗 -1
- F_GETFD/F_SETFD:獲取和設(shè)置文件描述符的標(biāo)志
- F_GETFL/F_SETFL:獲取和設(shè)置文件描述符的狀態(tài)標(biāo)志
P113 代碼清單中首先 F_GETFL 獲取 fd 的舊狀態(tài)標(biāo)志,然后 F_SETFL 將 fd 設(shè)置為非阻塞狀態(tài)
Ch.7 Linux 服務(wù)器程序規(guī)范
7.1 日志
7.1.1 Linux 系統(tǒng)日志
rsyslogd 守護(hù)進(jìn)程技能接收用戶進(jìn)程輸出的日志,又能接收內(nèi)核日志,通過調(diào)用 syslog 函數(shù)生成系統(tǒng)日志,該函數(shù)將日志輸出到一個(gè) UNIX 本地域 socket 類型 AF_UNIX 的 文件 /dev/log 中,具體參考 P115 Linux 的系統(tǒng)日志體系。
7.1.2 syslog 函數(shù)
#include <syslog.h>
// priority 是設(shè)施值(LOG_USER)與日志級別的按位或,7種日志級別參考 P115
void syslog(int priority, const char* message, ...);
openlog 可以改變 syslog 的默認(rèn)輸出方式,進(jìn)一步結(jié)構(gòu)化日志內(nèi)容
#include <syslog.h>
// ident 參數(shù)指定的字符串被添加到日志消息的日期和時(shí)間之后,一般為程序的名字
void openlog(const char* ident, int logopt, int facility);
程序開發(fā)過程中需要輸出很多調(diào)試信息,而發(fā)布之后又需要將這些調(diào)式信息關(guān)閉,這時(shí)候需要對日志進(jìn)行過濾
#include <syslog.h>
// 日志級別大于日志掩碼的日志信息會被系統(tǒng)忽略
int setlogmask(int maskpri);
// 最后需要關(guān)閉日志
void closelog();
7.2 用戶信息
7.2.1 uid/euid/gid/egid
#include <sys/types.h>
#include <unistd.h>
uid_t getuid(); // 獲取真實(shí)用戶 id
uid_t geteuid(); // 獲取有效用戶 id
gid_t getgid(); // 獲取真實(shí)組 id
gid_t getegid(); // 獲取有效組 id
P117 代碼清單 7-1 展示了 UID 和 EUID 的區(qū)別,代碼參考:7-1testeuid.cpp
7.2.2 切換用戶
代碼清單 7-2 展示了以 root 身份啟動的進(jìn)程切換為一個(gè)普通用戶身份運(yùn)行,沒看懂o(╥﹏╥)o
root 的 uid == 0 && guid == 0?
代碼參考:7-2switchuser.cpp
7.3 進(jìn)程間關(guān)系
7.3.1 進(jìn)程組
每個(gè)進(jìn)程都隸屬于一個(gè)進(jìn)程組,進(jìn)程組有進(jìn)程組 ID(PGID),首領(lǐng)進(jìn)程的 PID 和 PGID 相同
#include <unistd.h>
pid_t getpgid(pid_t pid); // 成功返回 pid 的進(jìn)程組的 PGID,失敗 -1
int setpgid(pid_t pid, pid_t pgid); // 設(shè)置 pid 的進(jìn)程組的 PGID 為 pgid,成功 0,失敗 -1
一個(gè)進(jìn)程只能設(shè)置自己或者子進(jìn)程的 PGID,并且子進(jìn)程調(diào)用 exec 系列函數(shù)之后不能再在父進(jìn)程中對它設(shè)置 PGID
7.3.2 會話
一些關(guān)聯(lián)的進(jìn)程組形成一個(gè)會話 session,創(chuàng)建會話
#include <unistd.h>
pid_t setsid(void); // 只能由非首領(lǐng)進(jìn)程創(chuàng)建會話,調(diào)用進(jìn)程成為會話的首領(lǐng)
pid_t getsid(pid_t pid); // 讀取會話ID SID,Linux 系統(tǒng)認(rèn)為 SID==PGID
7.3.3 ps 查看進(jìn)程關(guān)系
P119 圖 7-2 很清晰的展示了進(jìn)程之間的關(guān)系,不同進(jìn)程組成進(jìn)程組,不同進(jìn)程組組成會話
7.4 系統(tǒng)資源限制
Linux 系統(tǒng)有資源限制,比如物理設(shè)備限制、系統(tǒng)策略限制、具體實(shí)現(xiàn)的限制等等
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
// rlim_t 是一個(gè)整數(shù)類型,描述資源級別
struct rlimit
{
rlim_t rlim_cur; // 資源的軟限制,建議性的、最好不要超越的限制,超過可能會發(fā)信號終止進(jìn)程
rlim_t rlin_max; // 資源的硬限制,軟限制的上限,普通程序只能減少,只有 root 可以增加
}
ulimit 命令可以修改當(dāng)前 shell 環(huán)境下的資源限制,也可以通過修改配置文件來改變系統(tǒng)的軟限制和硬限制,這種修改是永久的。
7.5 改變工作目錄和根目錄
有些服務(wù)器程序還需要改變工作目錄和根目錄
#include <unistd.h>
// buf 指向內(nèi)存用于存儲進(jìn)程當(dāng)前工作目錄的絕對路徑名,size 指定其大小
// [return] 成功時(shí)返回一個(gè)指向目標(biāo)存儲區(qū)的指針
char* getcwd(char* buf, size_t size); // 失敗返回 NULL 并設(shè)置 errno
// path 指定要切換到的目標(biāo)目錄
int chdir(const char* path); // 成功 0,失敗 -1
// path 指定要切換到的目標(biāo)根目錄
int chroot(const char* path); // 成功 0,失敗 -1
7.6 服務(wù)器程序后臺化
如何讓進(jìn)程以守護(hù)進(jìn)程的方式運(yùn)行?參考代碼清單 7-3,實(shí)際上提供如下的系統(tǒng)調(diào)用:
#include <unistd.h>
// nochdir 用于指定是否改變工作目錄,0 --> 工作目錄設(shè)置為 "/",否則留在當(dāng)前目錄
// noclose 為 0 時(shí)標(biāo)準(zhǔn)輸入輸出以及錯(cuò)誤輸出都被重定向到 /dev/null 文件,否則依然使用原來的設(shè)備
int daemon(int nochdir, int noclose); // 成功 0,失敗-1
代碼參考:7-3daemonize.cpp
Ch.8 高性能服務(wù)器程序框架
8.1 服務(wù)器模型
8.1.1 C/S 模型
服務(wù)器客戶端模式
- 服務(wù)器:創(chuàng)建 socket --> bind 地址 --> listen --> select IO 復(fù)用 --> accept --> 邏輯單元(fork子進(jìn)程、子線程或其他)
- 客戶但:socket --> connect --> send --> recv
缺點(diǎn):訪問量過大時(shí),服務(wù)器負(fù)載加大,客戶端得到的響應(yīng)變慢
8.1.2 P2P 模型
點(diǎn)對點(diǎn)模式中主機(jī)即使服務(wù)端又是客戶端,缺點(diǎn)是:用戶之間傳輸?shù)恼埱筮^多時(shí),網(wǎng)絡(luò)的負(fù)載加重
通常 P2P 模型帶有一個(gè)專門的發(fā)現(xiàn)服務(wù)器,提供查找服務(wù)
8.2 服務(wù)器編程框架
- IO 處理單元:服務(wù)器管理客戶連接的模塊
- 邏輯單元:進(jìn)程或線程,分析并處理數(shù)據(jù),然后將結(jié)果傳遞給 IO 處理單元或者直接返回給客戶端
- 網(wǎng)絡(luò)存儲單元:數(shù)據(jù)庫、緩存或文件,可選的
8.3 IO 模型
阻塞 I/O 和 非阻塞 I/O,P126 描述了阻塞的 connect 工作流程,無法立即完成被系統(tǒng)掛起,直到等待的時(shí)間發(fā)生為止
- socket 相關(guān) API 中,可能被阻塞的系統(tǒng)調(diào)用包括 accept、send、recv 和 connect
- 非阻塞 I/O 執(zhí)行的系統(tǒng)調(diào)用如果時(shí)間沒有立即發(fā)生,返回 -1,這和出錯(cuò)的情況一樣,此時(shí)需要根據(jù) errno 來區(qū)分
- I/O 復(fù)用函數(shù)本身是阻塞的,它們能提高程序效率的原因在于它們具有同時(shí)監(jiān)聽多個(gè) I/O 事件的能力
同步 I/O 向應(yīng)用程序通知的是 I/O 就緒事件,異步 I/O 通知的是 I/O 完成事件
8.4 兩種高效的事件處理模式
同步 I/O 模型通常用于實(shí)現(xiàn) Reactor 模式,異步 I/O 模型則用于實(shí)現(xiàn) Proactor 模式
8.4.1 Reactor 模式
- 主線程(I/O 處理單元)只負(fù)責(zé)監(jiān)聽文件描述上是否有時(shí)間發(fā)生,有的話立即通知工作線程
- 工作線程(邏輯單元)讀寫數(shù)據(jù)、接收新的連接以及處理客戶端請求
P128 圖 8-5 展示同步 I/O epoll_wait 實(shí)現(xiàn)的 Reactor 模式,主線程通過 epoll_wait 監(jiān)聽到 socket 上有數(shù)據(jù)可讀或可寫時(shí),將這個(gè)可讀或可寫時(shí)間放入請求隊(duì)列,工作線程從請求隊(duì)列中取事件
8.4.2 Proactor 模式
- 主線程(I/O 處理單元)負(fù)責(zé)所有的 I/O 操作
- 工作線程(邏輯單元)僅僅負(fù)責(zé)業(yè)務(wù)邏輯
圖 8-6 展示了異步 I/O aio_read/aio_write 實(shí)現(xiàn)的 Proactor 模式,沒看懂o(╥﹏╥)o
8.4.3 模擬 Proactor 模式
使用同步 I/O epoll_wait 模擬 Proactor 模式,讓主線程執(zhí)行數(shù)據(jù)讀寫操作,讀寫完成之后,主線程向工作線程通知這一“完成事件”。那么從工作進(jìn)程的角度上來看,它們就直接獲得了數(shù)據(jù)的讀寫結(jié)果,接下來要做的就是對讀寫的結(jié)果進(jìn)行邏輯處理。
流程依舊沒看懂o(╥﹏╥)o
8.5 兩種高效的并發(fā)模式
8.5.1 半同步/半異步模式
- 同步:程序完成按照代碼序列的順序執(zhí)行
- 異步:程序的執(zhí)行需要由系統(tǒng)事件來驅(qū)動,常見的系統(tǒng)事件包括中斷、信號等
異步線程執(zhí)行效率高、實(shí)時(shí)性強(qiáng),但是難以調(diào)試和擴(kuò)展,不適合大量并發(fā);同步線程雖然執(zhí)行效率較低、實(shí)時(shí)性差,但邏輯簡單。服務(wù)器一般及要求較好的實(shí)時(shí)性,又要求能同時(shí)處理多個(gè)客戶請求的應(yīng)用程序,所以采用**「半同步/半異步模式」**實(shí)現(xiàn)
8.5.2 領(lǐng)導(dǎo)者/追隨者模式
多個(gè)工作線程輪流獲得事件源集合,輪流監(jiān)聽、分發(fā)并處理時(shí)間的一種模式。在任意時(shí)間點(diǎn),程序都僅有一個(gè)領(lǐng)導(dǎo)者線程,負(fù)責(zé)監(jiān)聽 I/O 事件。而其他線程都是追隨者,它們休眠在線程池中等待成為新的領(lǐng)導(dǎo)者。
- 句柄集 HandleSet
- 線程集 ThreadSet
- 時(shí)間處理器 EventHandler
8.6 有限狀態(tài)機(jī)
程序清單 8-3 展示了 HTTP 請求的讀取和分析中主從狀態(tài)機(jī)是如何處理 HTTP 請求字段的
// 代碼中有一個(gè) string.h 文件里面的庫函數(shù)
const char* str = "hello world, friend of mine!";
const char* sep = " ,!";
str = strpbrk(str, sep); // 找分隔符,str = " world, friend of mine!"
str += strspn(str, sep); // 跳過分隔符,str = "world, friend of mine!"
str = strchr(str, 'f'); // 找到第一個(gè)出現(xiàn)的字符,str = "friend of mine!"
代碼參考:8-3httpparser.cpp
8.7 提高服務(wù)器性能的其他建議
8.7.1 池
池通過空間換時(shí)間的思想來提高服務(wù)器的運(yùn)行效率,相當(dāng)于服務(wù)器管理系統(tǒng)資源的應(yīng)用設(shè)施,避免了服務(wù)器對內(nèi)核的頻繁訪問。
- 內(nèi)存池通常用于 socket 的接收緩存和發(fā)送緩存
- 進(jìn)程池和線程池通常用于并發(fā)編程,當(dāng)需要一個(gè)工作進(jìn)程或線程來處理新的客戶請求時(shí),可以直接從池中取得一個(gè)執(zhí)行實(shí)體,這樣就無需動態(tài)地調(diào)用 fork 或 pthread_create 等函數(shù)來創(chuàng)建進(jìn)程和線程
- 連接池通常用于服務(wù)器或服務(wù)器機(jī)群的內(nèi)部永久連接,連接池是服務(wù)器預(yù)先和數(shù)據(jù)庫程序建立的一組連接的集合
8.7.2 數(shù)據(jù)復(fù)制
內(nèi)核直接處理從 socket 或者文件讀入的數(shù)據(jù),所以應(yīng)用程序沒必要將這些數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用程序緩沖區(qū)中,使用“零拷貝”函數(shù) sendfile 等就可以將一個(gè)文件直接發(fā)送給客戶端。
另外當(dāng)兩個(gè)工作進(jìn)程之間需要傳遞大量的數(shù)據(jù)時(shí),應(yīng)考慮使用共享內(nèi)存來共享這些數(shù)據(jù),而不是使用管道或者消息隊(duì)列來傳遞,這樣就可以避免不必要的數(shù)據(jù)復(fù)制。
8.7.3 上下文切換和鎖
并發(fā)程序必須考慮上下文切換的問題,即進(jìn)程切換或線程切換導(dǎo)致的系統(tǒng)開銷;還需要考慮共享資源的加鎖保護(hù),如果服務(wù)器必須使用“鎖”,則可以考慮減小鎖的粒度。
Ch.9 I/O 復(fù)用
9.1 select 系統(tǒng)調(diào)用
select 系統(tǒng)調(diào)用的原型如下:
#include <sys/select.h>
// nfds 指定被監(jiān)聽的文件描述符的總數(shù),通常是監(jiān)聽的所有文件描述符中的最大值加1
// readfds, writefds, exceptfds 分別指向可讀、可寫和異常等事件對應(yīng)的文件描述符集合
// timeout 設(shè)置 select 函數(shù)的超時(shí)時(shí)間,0 立即返回,NULL 一直阻塞
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
// fd_set 結(jié)構(gòu)體僅包含一個(gè)整形數(shù)組,該數(shù)組的每個(gè)元素的每一位 bit 標(biāo)記一個(gè)文件描述符,容納數(shù)量由 FD_SETSIZE 指定
FD_ZERO(fd_set* fdset); // 清除 fdset 的所有位
FD_SET(int fd, fd_set *fdset); // 設(shè)置 fdset 的位 fd
FD_CLR(int fd, fd_set *fdset); // 清除 fdset 的位 fd
int FD_ISSET(int fd, fd_set *fdset); // 測試 fdset 的位 fd 是否被設(shè)置
struct timeval
{
long tv_sec; // 秒數(shù)
long tv_usec; // 微秒數(shù)
}
select 成功時(shí)返回就緒文件描述符的總數(shù),如果在超時(shí)時(shí)間內(nèi)沒有任何文件描述符就緒就返回 0,失敗 -1 并設(shè)置 errno
文件描述符可讀就緒條件
- socket 接收緩沖區(qū)中的字節(jié)數(shù)大于或等于低水位標(biāo)記 SO_RCVLOWAT 時(shí)可以無阻塞地讀 socket
- socket 通信對方關(guān)閉連接時(shí),對該 socket 的讀操作返回 0
- 監(jiān)聽 socket 上有新的連接
- socket 上有未處理的錯(cuò)誤
文件描述符可寫就緒條件
- socket 發(fā)送緩沖區(qū)中的可用字節(jié)數(shù)大于或等于低水位標(biāo)記 SO_RCVLOWAT 時(shí)可以無阻塞地寫 socket
- socket 的寫操作被關(guān)閉,對該 socket 的讀操作返回 0
- socket 使用非阻塞 connect 連接成功或者失?。ǔ瑫r(shí))之后
- socket 上有未處理的錯(cuò)誤
代碼參考:9-1use_select.cpp
9.2 poll 系統(tǒng)調(diào)用
poll 和 select 類似,也是在指定時(shí)間內(nèi)輪詢一定數(shù)量的文件描述符,測試其是否就緒
#include <poll.h>
// fds 參數(shù)指定感興趣的文件描述符上發(fā)生的可讀、可寫和異常事件
// nfds 參數(shù)指定被監(jiān)聽事件集合 fds 的大小,實(shí)際類型為 unsigned long int
// timeout 指定 poll 的超時(shí)值,單位是毫秒,-1 永遠(yuǎn)阻塞,0 直接返回
// [return] 和 select 一樣
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
// pollfd 結(jié)構(gòu)體
struct pollfd
{
int fd; // 文件描述符
short events; // 注冊的事件,一系列 POLL 事件的按位或
short revents; // 實(shí)際發(fā)生的事件,內(nèi)核填充
}
9.3 epoll 系列系統(tǒng)調(diào)用
Mac 里沒有 epoll 庫,使用 kqueue 代替,參考
epoll 把用戶關(guān)心的文件描述符上的事件放入內(nèi)核里的一個(gè)時(shí)間表中,無需像 select 和 poll 那樣每次調(diào)用都要重復(fù)傳入文件描述符集或事件集。epoll 需要一個(gè)額外的文件描述符來唯一標(biāo)識內(nèi)核中的這個(gè)事件表
#include <sys/epoll.h>
// 創(chuàng)建標(biāo)識內(nèi)核中的事件表,size 參數(shù)并無實(shí)際作用
// [return] 返回的 fd 將作為其他所有 epoll 系統(tǒng)調(diào)用的第一個(gè)參數(shù)
int epoll_create(int size);
// 操作內(nèi)核事件表
// op 參數(shù)指定操作類型,由 EPOLL_CTL_ADD|EPOLL_CTL_MOD|EPOLL_CTL_DEL 組成
// fd 參數(shù)是要操作的文件描述符
// event 參數(shù)指定事件
// [return] 成功 0,失敗 -1 并設(shè)置 errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
_uint32_t events; // epoll 事件,和 poll 類型基本一致
epoll_data_t data; // 用戶數(shù)據(jù)
}
// 聯(lián)合體
typedef union epoll_data {
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
// timeout 指定超時(shí),maxevents 指定最多監(jiān)聽多少個(gè)事件
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
epoll 相比于 poll 有兩個(gè)額外的事件類型:EPOLLET | EPOLLONESHOT
epoll_wait 將所有就緒的事件從內(nèi)核事件表(由 epfd 參數(shù)指定)中復(fù)制到 events 指定的數(shù)組中,只用于輸出 epoll_wait 檢測到的就緒事件,所以相比于 select 和 poll 極大提升性能
LT 和 ET 模式
- LT:Level Trigger,默認(rèn)的,epoll_wait 檢測到其上有事件發(fā)生并將此事件通知應(yīng)用程序之后,應(yīng)用程序可以不立即處理該事件,下次調(diào)用 epoll_wait 還可以再次向應(yīng)用程序通告此事件
- ET:Edge Trigger,epoll_wait 檢測到就緒事件之后必須處理,效率比 LT 模式要高,需要指定 EPOLLET 事件類型
代碼參考:LT vs ET 9-3mtlt.cpp
EPOLLONESHOT 事件
即使在 ET 模式下,一個(gè) socket 上的某個(gè)事件還是可能被觸發(fā)多次,在并發(fā)程序中,一個(gè)線程讀取完某個(gè) socket 上的數(shù)據(jù)后開始處理這些數(shù)據(jù),但是在處理過程中該 socket 上又有新數(shù)據(jù)可讀(EPOLLIN 被再次觸發(fā)),此時(shí)另一個(gè)線程被喚醒來讀取這些新的數(shù)據(jù)。此時(shí)出現(xiàn)了兩個(gè)線程同時(shí)操作一個(gè) socket 的局面
為此需要 EPOLLONESHOT 事件
代碼參考:9-4oneshot.cpp
9.4 三組 I/O 復(fù)用函數(shù)比較
selecet、poll 和 epoll 都通過某種結(jié)構(gòu)體變量來告訴內(nèi)核監(jiān)聽哪些文件描述符上的哪些事件,并使用該結(jié)構(gòu)體類型的參數(shù)來獲取內(nèi)核處理的結(jié)果
- select 參數(shù)類型 fd_set 沒有將文件描述符和事件綁定,因此需要 3 個(gè)類型的參數(shù)分別區(qū)分可讀、可寫和異常事件,不能處理更多類型的事件,且下次調(diào)用時(shí)需要重置 3 個(gè) fd_set 集合
- poll 通過參數(shù)類型 pollfd 將文件描述符和事件都定義在其中,支持更多的事件類型,且下次調(diào)用 poll 時(shí)無需重置 pollfd 類型的事件集參數(shù),因?yàn)閮?nèi)核修改的僅僅是 revents 成員
- select 和 poll 調(diào)用返回整個(gè)用戶注冊的事件集合(包括就緒和未就緒的),索引就緒文件描述符的時(shí)間復(fù)雜度為 O(n),epoll 通過 epoll_wait 直接從 epollfd 指定的內(nèi)核事件表中取得用戶注冊的事件,且通過 events 參數(shù)僅僅用來返回就緒的事件,索引就緒的 fd 事件復(fù)雜度為 O(1)
- poll 和 epoll_wait 分別使用 nfds 和 maxevents 參數(shù)指定最多監(jiān)聽的 fd 和 事件,最大 65535,但是 select 一般是 1024
- poll 和 selecet 只能工作在相對低效的 LT 模式,epoll 可以在 ET 模式,且還支持 EPOLLONESHOT 事件
具體區(qū)別參考表格 9-2
9.5 I/O 復(fù)用的高級應(yīng)用一:非阻塞 connect
connect 出錯(cuò)時(shí)有一個(gè) errno 值:EINPROGRESS,這種錯(cuò)誤發(fā)生在非阻塞的 sockct 調(diào)用 connect,而連接又沒有立即建立時(shí)。根據(jù) man 文檔的解釋,在這種情況下,我們可以調(diào)用 select、poll 等函數(shù)來監(jiān)聽這個(gè)連接失敗的 socket 上的可寫事件。當(dāng)select、poll 等函數(shù)返回后,再利用 getsockopt 來讀取錯(cuò)誤碼并清除該 socket 上的錯(cuò)誤。如果錯(cuò)誤碼是0,表示連接成功建立,否則連接失敗。
代碼參考:9-5unblockconnect.cpp
9.6 I/O 復(fù)用的高級應(yīng)用二:聊天室程序
客戶端程序有兩個(gè)功能:
- 從標(biāo)準(zhǔn)輸人終端讀入用戶數(shù)據(jù),并將用戶數(shù)據(jù)發(fā)送至服務(wù)器
- 往標(biāo)準(zhǔn)輸出終端打印服務(wù)器發(fā)送給它的數(shù)據(jù)
代碼參考:9-6mytalk_client.cpp
服務(wù)器的功能
- 接收客戶數(shù)據(jù)
- 把客戶數(shù)據(jù)發(fā)送給每一個(gè)登錄到該服務(wù)器上的客戶端(數(shù)據(jù)發(fā)送者除外)
代碼參考:9-7mytalk_server.cpp
9.7 I/O 復(fù)用的高級應(yīng)用二:同時(shí)處理 TCP 和 UDP 服務(wù)
代碼參考:9-8multi_port.cpp
9.8 超級服務(wù) xinetd
Linux 因特網(wǎng)服務(wù) inetd 是超級服務(wù)。它同時(shí)管理著多個(gè)子服務(wù),即監(jiān)聽多個(gè)遄口?,F(xiàn)在 Linux 系統(tǒng)上使用的 inetd 服務(wù)程序通常是其升級版本 xinetd。 xinetd 程序的原理與 inetd 相同,但增加了一些控制選項(xiàng),并提高了安全性。
Ch.10 信號
信號是由用戶、系統(tǒng)或者進(jìn)程發(fā)送給目標(biāo)進(jìn)程的信息,以通知目標(biāo)進(jìn)程某個(gè)狀態(tài)的改變或系統(tǒng)異常。Linux 信號可由如下條件產(chǎn)生:
- 對于前臺進(jìn)程,用戶可以通過輸人特殊的終端字符來給它發(fā)送信號。比如輸入 Ctrl+C 通常會給進(jìn)程發(fā)送一個(gè)中斷信號
- 系統(tǒng)異常。比如浮點(diǎn)異常和非法內(nèi)存段訪問
- 系統(tǒng)狀態(tài)變化。比如 alarm 定時(shí)器到期將引起 SIGALRM 信號
- 運(yùn)行 kill 命令或調(diào)用 kill 函數(shù)
10.1 Linux 信號概述
#include <sys/types.h>
#include <signal.h>
// 把信號 sig 發(fā)給目標(biāo)進(jìn)程 pid
int kill(pid_t pid, int sig); // 成功返回 0,失敗 -1 并設(shè)置 errno
// 信號處理函數(shù)原型
typedef void (*__sighandler_t)(int);
linux 信號有很多,和網(wǎng)絡(luò)編程關(guān)系緊密的是:
- SIGHUP:控制終端掛起
- SIGPIPE:往讀端被關(guān)閉的管道或者 socket 連接中寫數(shù)據(jù)
- SIGURG:socket 連接上接受到緊急數(shù)據(jù)
- SIGALRM:由 alarm 或 setitimer 設(shè)置的實(shí)時(shí)鬧鐘超時(shí)引起
- SIGCHLD:子進(jìn)程狀態(tài)發(fā)生變化(退出或暫停)
10.2 信號函數(shù)
signal 系統(tǒng)調(diào)用
#include <signal.h>
// sig 參數(shù)指出要捕獲的信號類型,
// _handler 參數(shù)是函數(shù)指針,用于指定信號 sig 的處理函數(shù)
_sighandler_t signal(int sig, _sighandler_t handler);
sigaction 系統(tǒng)調(diào)用
#include <signal.h>
// sig 指出要捕獲的信號類型
// act 參數(shù)指定新的信號處理方式
// oact 輸出信號先前的處理方式(如果不為 NULL 的話)
// [return] 成功 0,失敗 -1 并設(shè)置 errno
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);
struct sigaction {...}; // 參考 P181
10.3 信號集
信號集函數(shù)
#include <bits/sigset.h>
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
tydedef struct {
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t; // 其實(shí)就是一個(gè)長整型數(shù)組
進(jìn)程信號掩碼
#include <signal.h>
// _set 參數(shù)指定新的信號掩碼,_how 指定設(shè)置掩碼方式 SIG_BLOCK|SIG_UNBLOCK|SIG_SETMASK
// _oset 參數(shù)輸出原來的信號掩碼(如果不為 NULL 的話)
// [return] 成功 0,失敗 -1 并設(shè)置 errno
int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset);
被掛起的信號
設(shè)置進(jìn)程信號掩碼后,被屏蔽的信號將不能被進(jìn)程接收。如果給進(jìn)程發(fā)送一個(gè)被屏蔽的信號,則操作系統(tǒng)將該信號設(shè)置為進(jìn)程的一個(gè)被掛起的信號。如果我們?nèi)∠麑Ρ粧炱鹦盘柕钠帘危瑒t它能立即被進(jìn)程接收到。如下兩數(shù)可以獲得進(jìn)程當(dāng)前被掛起的信號集
#include <signal.h>
// 獲取進(jìn)程當(dāng)前被掛起的信號集
// [return] 成功 0,失敗 -1 并設(shè)置 errno
int sigpending(sigset_t* set);
10.4 統(tǒng)一事件源
信號是一種異步事件:信號處理兩數(shù)和程序的主循環(huán)是兩條不同的執(zhí)行路線。很顯然,信號處理函數(shù)需要盡可能快地執(zhí)行完畢,以確保該信號不被屏蔽(前面提到過,為了避免一些競態(tài)條件,信號在處理期間,系統(tǒng)不會再次觸發(fā)它)太久。一種典型的解決方案是:
把信號的主要處理邏輯放到程序的主循環(huán)中,當(dāng)信號處理函數(shù)被觸發(fā)時(shí),它只是簡單地通知主循環(huán)程序接收到信號,并把信號值傳遞給主循環(huán),主循環(huán)再根據(jù)接收到的信號值執(zhí)行目標(biāo)信號對應(yīng)的邏輯代碼。信號處理函數(shù)通常使用管道來將信號 “傳遞”給主循環(huán):信號處理函數(shù)往管道的寫端寫人信號值,主循環(huán)則從管道的讀端讀出該信號值。那么主循環(huán)怎么知道管道上何時(shí)有數(shù)據(jù)可讀呢?這很簡單,我們只需要使用 I/O 復(fù)用系統(tǒng)調(diào)用來監(jiān)聽管道的讀端文件描述符上的可讀事件。如此一來,信號事件就能和其他 I/O 事件一樣被處理,即統(tǒng)一事件源。
代碼參考:10-1unievent.cpp
10.5 網(wǎng)絡(luò)編程相關(guān)的信號
SIGHUP:書中展示了 xinetd 程序接收并處理 SIGHUP 信號的流程
SIGPIPE:往一個(gè)讀端關(guān)閉的管道或 socket 連接中寫數(shù)據(jù)會引發(fā) SIGPIPE 信號,兩種方式檢測該信號:
- send 函數(shù)加入 MSG_NOSIGNAL 標(biāo)志可以進(jìn)制寫操作觸發(fā) SIGPIPE 信號,否則就會失敗并設(shè)置 errno,根據(jù) errno 值來判斷管道或者 socket 連接的讀端是否已經(jīng)關(guān)閉
- poll 系統(tǒng)調(diào)用在管道讀端關(guān)閉時(shí),寫端文件描述符上的 POLLHUP 會被觸發(fā),socket 被對方關(guān)閉時(shí),socket 上的 POLLRDHUP 事件會被觸發(fā)
SIGURG:socket 接收到緊急數(shù)據(jù)時(shí)觸發(fā),兩種方式檢測帶外數(shù)據(jù):
- select 系統(tǒng)調(diào)用接收到帶外數(shù)據(jù)返回時(shí)會向應(yīng)用程序報(bào)告 socket 上的異常事件 exception_fds
- 檢測 SIGURG 信號,設(shè)置該信號的處理函數(shù)
Ch.11 定時(shí)器
11.1 socket 選項(xiàng) SO_RCVTIMEO/SO_SNDTIMEO
這兩個(gè)參數(shù)分別表示 socket 接受數(shù)據(jù)超時(shí)時(shí)間和發(fā)送數(shù)據(jù)超時(shí)時(shí)間,這個(gè)選項(xiàng)只針對 send, sendmsg, recv, recvmsg, accept, connect 有效
代碼參考:11-1connect_timeout.cpp
11.2 SIGALRM 信號
第10章提到,由alarm 和 setitimer 兩數(shù)設(shè)置的實(shí)時(shí)鬧鐘一旦超時(shí),將觸發(fā) SIGALRM 信號。因此可以利用該信號的信號處理函數(shù)來處理定時(shí)任務(wù)。但是,如果要處理我個(gè)定時(shí)任務(wù),我們就需要不斷地觸發(fā) SIGALRM 信號,并在其信號處理函數(shù)中執(zhí)行到期的任務(wù)。一般而言,SIGALRM 信號按照固定的頻率生成,即由 alarm 或 setitimer 函數(shù)設(shè)置的定時(shí)周期 T 保持不變。如果某個(gè)定時(shí)任務(wù)的超時(shí)時(shí)間不是 T 的整數(shù)倍,那么它實(shí)際被執(zhí)行的時(shí)間和預(yù)期的時(shí)間將略有偏差。
11.2.1 基于升序鏈表的定時(shí)器
其核心函數(shù) tick 相當(dāng)于一個(gè)心搏函數(shù),它每隔一段固定的時(shí)間就執(zhí)行一次,以檢測并處理到期的任務(wù)。判斷定時(shí)任務(wù)到期的依據(jù)是定時(shí)器的 expire 值小于當(dāng)前的系統(tǒng)時(shí)間。從執(zhí)行效率來看,添加定時(shí)器的時(shí)向復(fù)雜度是 O(n),刪除定時(shí)器的時(shí)間復(fù)雜度是 O(1),執(zhí)行定時(shí)任務(wù)的時(shí)間復(fù)雜度是 O(1)
代碼參考:11-2lst_timer.h
11.2.2 處理非活動連接
服務(wù)器程序通常要定期處理非活動連接:給客戶端發(fā)一個(gè)重連請求,或者關(guān)閉該連接,或者其他。Linux 在內(nèi)核中提供了對連接是否處于活動狀態(tài)的定期檢查機(jī)制,我們可以通過 socket 選項(xiàng) KEEPALIVE 來激活它。不過使用這種方式將使得應(yīng)用程序?qū)B接的管理變得復(fù)雜。因此,我們可以考慮在應(yīng)用層實(shí)現(xiàn)類似于 KEEPALIVE 的機(jī)制,以管理所有長時(shí)間處于非活動狀態(tài)的連接。代碼清單 11-3 利用alarm 函數(shù)周期性地觸發(fā) SIGALRM 信號,該信號的信號處理函數(shù)利用管道通知主循環(huán)執(zhí)行定時(shí)器鏈表上的定時(shí)任務(wù)——關(guān)閉非活動的連接。
代碼參考:11-3nonactive_conn.cpp
11.3 I/O 復(fù)用系統(tǒng)調(diào)用的超時(shí)參數(shù)
Linux 下的 3 組 I/O 復(fù)用系統(tǒng)調(diào)用都帶有超時(shí)參數(shù),因此它們不僅能統(tǒng)一處理信號和 I/O 事件,也能統(tǒng)一處理定時(shí)事件。但是由于 I/O 復(fù)用系統(tǒng)調(diào)用可能在超時(shí)時(shí)間到期之前就返回(有 I/O 事件發(fā)生),所以如果我們要利用它們來定時(shí),就需要不斷更新定時(shí)參數(shù)以反映剩余的時(shí)間
代碼參考:11-4io_timer.cpp
11.4 高性能定時(shí)器
時(shí)間輪
前面的基于排序鏈表的定時(shí)器添加定時(shí)器的效率偏低,為此考慮更高效的時(shí)間輪
基于排序鏈表的定時(shí)器使用唯一的一條鏈表來管理所有定時(shí)器,所以插人操作的效率隨著定時(shí)器數(shù)目的增多而降低。而時(shí)間輪使用哈希表的思想,將定時(shí)器散列到不同的鏈表上。這樣每條鏈表上的定時(shí)器數(shù)目都將明顯少于原來的排序鏈表上的定時(shí)器數(shù)目,插人操作的效率基本不受定時(shí)器數(shù)目的影響。
代碼參考:11-5tw_timer.h
時(shí)間堆
前面討論的定時(shí)方案都是以固定的頻率調(diào)用心搏函數(shù) tick,并在其中依次檢測到期的定時(shí)器,然后執(zhí)行到期定時(shí)器上的回調(diào)函數(shù)。設(shè)計(jì)定時(shí)器的另外一種思路是:將所有定時(shí)器中超時(shí)時(shí)間最小的一個(gè)定時(shí)器的超時(shí)值作為心搏間隔。這樣,一旦心搏函數(shù) tick 被調(diào)用,超時(shí)時(shí)間最小的定時(shí)器必然到期,我們就可以在 tick 函數(shù)中處理該定時(shí)器。然后,再次從剩余的定時(shí)器中找出超時(shí)時(shí)間最小的一個(gè),并將這段最小時(shí)間設(shè)置為下一次心搏間隔。如此反復(fù),就實(shí)現(xiàn)了較為精確的定時(shí)。
代碼參考:11-6time_heap.h 沒太看懂
Ch.12 高性能 I/O 框架庫 Libevent
12.1 I/O 框架庫概述
- 句柄:I/O 框架庫要處理的對象,即 I/O 事件、信號和定時(shí)事件
- 事件多路分發(fā)器:I/O 框架庫一般將系統(tǒng)支持的各種 I/O 復(fù)用系統(tǒng)調(diào)用封裝成統(tǒng)一的接口
- 事件處理器:包含一個(gè)或多個(gè) handle_event 回調(diào)函數(shù)
- 具體事件處理器:繼承事件處理器的接口實(shí)現(xiàn)自己的事件處理器
12.2 Libevent 源碼分析
TODO 略
Ch.13 多進(jìn)程編程
13.1 fork 系統(tǒng)調(diào)用
#include <sys/types.h>
#include <unistd.h>
// 返回兩次,父進(jìn)程返回的子進(jìn)程的 PID,子進(jìn)程中返回 0,失敗 -1
pid_t fork(void );
fork 函數(shù)復(fù)制當(dāng)前進(jìn)程,在內(nèi)核進(jìn)程表中創(chuàng)建一個(gè)新的進(jìn)程表項(xiàng)。新的進(jìn)程表項(xiàng)有很多屬性和原進(jìn)程相同,比如堆指針、棧指針和標(biāo)志寄存器的值。但也有許多屬性被賦予了新的值,比如該進(jìn)程的 PPID 被設(shè)置成原進(jìn)程的 PID,信號位圖被清除(原進(jìn)程設(shè)置的信號處理函數(shù)不再對新進(jìn)程起作用)
13.2 exec 系列系統(tǒng)調(diào)用
需要在子進(jìn)程種執(zhí)行其他程序,即替換當(dāng)前進(jìn)程映像,就需要使用 exec 系列函數(shù)
#include <unistd.h>
extern char** environ;
// path 參數(shù)指定可執(zhí)行文件的完整路徑
// file 參數(shù)接受文件名
// avg 接受可變參數(shù),argv 接受參數(shù)數(shù)組
// envp 設(shè)置新程序的環(huán)境變量
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execv(const char* path, const char* argv[] );
int execvp(const char* file, const char* argv[] );
int execvpe(const char* path, const char* argv[], char* const envp[]);
一般情況下,exec 函數(shù)是不返回的,除非出錯(cuò)。它出錯(cuò)時(shí)返回-1,并設(shè)置 errno。 如果沒出錯(cuò),則原程序中 exec 調(diào)用之后的代碼都不會執(zhí)行,因?yàn)榇藭r(shí)原程序己經(jīng)被 exec 的參數(shù)指定的程序完全替換(包括代碼和數(shù)據(jù))。exec 函數(shù)不會關(guān)閉原程序打開的文件描述符,除非該文件描述符被設(shè)置了類似 SOCK_CLOEXEC 的屬性(見5.2節(jié))。
13.3 處理僵尸進(jìn)程
兩種情況:
- 在子進(jìn)程結(jié)束運(yùn)行之后,父進(jìn)程讀取其退出狀態(tài)之前,該子進(jìn)程處于僵尸態(tài)
- 父進(jìn)程結(jié)束或者異常終止,而子進(jìn)程繼續(xù)運(yùn)行。此時(shí)子進(jìn)程的 PPID 將被操作系統(tǒng)設(shè)置為 1,即 init 進(jìn)程。init 進(jìn)程接管了該子進(jìn)程,并等待它結(jié)束。
通過下面的系統(tǒng)調(diào)用,在父進(jìn)程中調(diào)用等待子進(jìn)程的結(jié)束,并獲取子進(jìn)程的返回信息,從而避免僵尸進(jìn)程的產(chǎn)生,或者使子進(jìn)程的僵尸狀態(tài)立即結(jié)束
#include <sys/types.h>
#include <sys/wait.h>
// 阻塞
pid_t wait(int* stat_loc);
// 只等待 pid 指定的子進(jìn)程,為 -1 時(shí)與 wait 一樣
// options 指定 WNOHANG 參數(shù)可以非阻塞:如果 pid 子進(jìn)程還未結(jié)束或者意外終止直接返回 0
pid_t waitpid(pid_t pid, int* stat_loc, int options);
要在事件已經(jīng)發(fā)生的情說下執(zhí)行非阻基調(diào)用才能提高程序的效率。對 waitpid 函數(shù)而言,我們最好在某個(gè)子進(jìn)程退出之后再調(diào)用它。那么父進(jìn)程從何得知某個(gè)子進(jìn)程已經(jīng)退出了呢?這正是 SIGCHLD 信號的用途。當(dāng)一個(gè)進(jìn)程結(jié)束時(shí),它將給其父進(jìn)程發(fā)送一個(gè)SIGCHLD 信號。因此,我們可以在父進(jìn)程中捕獲SIGCHLD 信號,并在信號處理函數(shù)中調(diào)用 waitpid 函數(shù)以“徹底結(jié)束”一個(gè)子進(jìn)程
13.4 管道
管道能在父子進(jìn)程間傳遞數(shù)據(jù),一般來說是單向的,只能保證父子進(jìn)程之間一個(gè)方向的數(shù)據(jù),父子進(jìn)程必須有一個(gè)關(guān)閉 fd[0]、另一個(gè)關(guān)閉 fd[1],如果要實(shí)現(xiàn)父子進(jìn)程之間的雙向數(shù)據(jù)傳輸就必須使用兩個(gè)管道。
書中描述了 squid 如何使用 socketpair 系統(tǒng)調(diào)用創(chuàng)建一個(gè)全雙工管道的
13.5 信號量
信號量原語
關(guān)鍵代碼段/臨界區(qū)代碼會引發(fā)進(jìn)程之間的競態(tài)條件,進(jìn)程同步需要確保任一時(shí)刻只有一個(gè)進(jìn)程能進(jìn)入關(guān)鍵代碼段。Dekker 算法和 Peterson 算法通過忙等待解決同步問題,CPU 利用率低;Dijkstra 提出的信號量(Semaphore)通過 P、V 操作實(shí)現(xiàn)。
信號量取值可以是任何自然數(shù),最常用的 0 1 是Mutex,Linux 相關(guān)的系統(tǒng)調(diào)用是 semget、semop 和 semctl,內(nèi)核中與信號量相關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)是 semid_ds
semget
semget 系統(tǒng)調(diào)用創(chuàng)建一個(gè)新的信號量集(會修改內(nèi)核數(shù)據(jù)結(jié)構(gòu)體 semid_ds),或者獲取一個(gè)已經(jīng)存在的信號量集
#include <sys/sem.h>
// key 參數(shù)標(biāo)識一個(gè)全局唯一的信號量集合
// num_sems 指定要創(chuàng)建/獲取的信號量集中信號量的數(shù)目,0 標(biāo)識獲取已經(jīng)存在的信號量
// sem_flags 參數(shù)指定一組標(biāo)志
// [return] 成功返回一個(gè)正整數(shù)表示信號量集的標(biāo)識符,失敗 -1 并設(shè)置 errno
int semget(ket_t key, int num_sems, int sem_flags);
key 可以傳遞一個(gè)特殊的鍵值 IPC_PRIVATE(值為0),這樣無論信號量是否已經(jīng)存在,semget 都將創(chuàng)建一個(gè)新的信號量。所有其他進(jìn)程都可以使用這個(gè)新創(chuàng)建的信號量
semop
#include <sys/sem.h>
// sem_id 是 semget 返回的信號量集標(biāo)識符
// sem_ops 指向一個(gè) sembuf 結(jié)構(gòu)體類型的數(shù)組
// num_sem_ops 指定要執(zhí)行的操作個(gè)數(shù)
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
這個(gè) sembuf 結(jié)構(gòu)體中 sem_flg 和 sem_op 關(guān)系有點(diǎn)復(fù)雜,沒看懂o(╥﹏╥)o
semctl
#include <sys/sem.h>
// sem_id 是 semget 返回的信號量集標(biāo)識符
// sem_num 指定被操作的信號量在信號量集中的編號
// command 指定要執(zhí)行的命令
int semctl(int sem_id, int sem_num, int command, ...);
代碼參考:13-3sem.cpp
13.6 共享內(nèi)存
共享內(nèi)存是最高效的 IPC 機(jī)制,他不涉及進(jìn)程之間的任何數(shù)據(jù)傳輸,內(nèi)核中與共享內(nèi)存相關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)是 shmid_ds
shmget
和 semget 一樣,shmget 系統(tǒng)調(diào)用創(chuàng)建一段新的共享內(nèi)存或者獲取一段已經(jīng)存在的共享內(nèi)存
#include <sys/shm.h>
// key 表示一段全局唯一的共享內(nèi)存
// size 指定共享內(nèi)存的大小,單位 byte,為 0 表示獲取已經(jīng)存在的共享內(nèi)存
// shmflg 和 sem_flags 類似
// [return] 成功返回一個(gè)正整數(shù)標(biāo)識共享內(nèi)存的標(biāo)識符,失敗 -1 并設(shè)置 errno
int shmget(key_t key, size_t size, int shmflg);
shmat 和 shmdt
共享內(nèi)存創(chuàng)建之后需要先將其關(guān)聯(lián)到進(jìn)程的地址空間才能使用
// shm_id 是由 shmget 返回的共享內(nèi)存標(biāo)識符
// shm_addr 指定共享內(nèi)存關(guān)聯(lián)到進(jìn)程的那塊地址空間
// shmflg 是一些標(biāo)志 SHM_RND|SHM_RDONLY...
// [return] 成功時(shí)返回共享內(nèi)存被關(guān)聯(lián)到的地址,失敗返回 -1 并設(shè)置 errno
void* shmat(int shm_id, const void* shm_addr, int shmflg);
使用完共享內(nèi)存之后還需要將它從進(jìn)程地址空間分離
// 將關(guān)聯(lián)到的 shm_addr 處的共享內(nèi)存從進(jìn)程中分離,失敗 -1 并設(shè)置 errno
int shmdt(const void* shm_addr);
shmctl
#include <sys/shm.d>
// shm_id 是 shmget 返回的共享內(nèi)存標(biāo)識符
// command 指定要執(zhí)行的命令
// [return] 成功返回值取決于 command,失敗 -1 并設(shè)置 errno
int shmctl(int shm_id, int command, struct shmid_ds* buf);
共享內(nèi)存的POSIX方法
mmap 函數(shù)利用它的 MAP ANONYMOUS 標(biāo)志我們可以實(shí)現(xiàn)父、子進(jìn)程之間的匿名內(nèi)存共享。通過打開同一個(gè)文件,mmap 也可以實(shí)現(xiàn)無關(guān)進(jìn)程之間的內(nèi)存共享。Linux 提供了另外一種利用mmap 在無關(guān)進(jìn)程之間共享內(nèi)存的方式。這種方式無需任何文件的支特,但它需要先使用如下函數(shù)來創(chuàng)建或打開一個(gè) POSIX 共享內(nèi)存對象:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
// 與 open 系統(tǒng)調(diào)用完全相同,shm_open 成功時(shí)返回一個(gè)文件描述符,失敗 -1 并設(shè)置 errno
int shm_open(const char* name, int oflag, mode_t mode);
// shm_open 創(chuàng)建的共享內(nèi)存對象使用完之后需要刪除
int shm_unlink(const char* name);
這里實(shí)現(xiàn)多進(jìn)程的聊天服務(wù)器:一個(gè)子進(jìn)程處理一個(gè)客戶連接,所有客戶 socket 連接的讀緩沖設(shè)置為一塊共享內(nèi)存,實(shí)現(xiàn)”共享讀“,每個(gè)子進(jìn)程在使用共享內(nèi)存時(shí)無需加鎖
代碼參考:13-4shm_talk_server.cpp
13.7 消息隊(duì)列
消息隊(duì)列是在兩個(gè)進(jìn)程之間傳遞二進(jìn)制塊數(shù)據(jù)的一種簡單有效的方式。每個(gè)數(shù)據(jù)塊都有一個(gè)特定的類型,接收方可以根據(jù)類型來有選擇地接收數(shù)據(jù),而不一定像管道和命名管道那樣必須以先進(jìn)先出的方式接收數(shù)據(jù)。
同樣的有 4 個(gè)系統(tǒng)調(diào)用:
#include <sys/msg.h>
// 創(chuàng)建一個(gè)消息隊(duì)列,或者獲取一個(gè)已有的消息隊(duì)列
int msgget(key_t key, int msgflg);
// 將一條消息 msg_ptr 添加到消息隊(duì)列
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);
// 從消息隊(duì)列中獲取消息 msg_ptr
int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
// 控制消息隊(duì)列的某些屬性
int msgctl(int msqid, int command, struct msqid_ds* buf);
13.8 IPC 命令
ipcs 命令可以顯示 Linux 系統(tǒng)擁有的共享內(nèi)存、信號量和消息隊(duì)列資源使用情況
13.9 在進(jìn)程間傳遞文件描述符
由于 fork 調(diào)用之后,父進(jìn)程中打開的文件描述符在子進(jìn)程中仍然保持打開,所以文件描述符可以很方便地從父進(jìn)程傳遞到子進(jìn)程。需要注意的是,傳遞一個(gè)文件描述符并不是傳遞一個(gè)文件描述符的值,而是要在接收進(jìn)程中創(chuàng)建一個(gè)新的文件描述符,并且該文件描述符和發(fā)送進(jìn)程中被傳遞的文件描述符指向內(nèi)核中相同的文件表項(xiàng)。
那么如何把子進(jìn)程中打開的文件描述符傳遞給父進(jìn)程呢?或者更通俗地說,如何在兩個(gè)不相于的進(jìn)程之間傳遞文件描述符呢?在 Linux 下,我們可以利用 UNIX 域 socket 在進(jìn)程間傳遞特殊的輔助數(shù)據(jù),以實(shí)現(xiàn)文件描述符的傳遞。代碼清單 13-5 給出了一個(gè)實(shí)例,它在子進(jìn)程中打開一個(gè)文件描述符,然后將它傳遞給父進(jìn)程,父進(jìn)程則通過讀取該文件描述符來獲得文件的內(nèi)容。
代碼參考:13-5passfd.cpp
Ch.14 多線程編程
14.1 Linux 線程概述
線程模型
- 內(nèi)核線程:運(yùn)行在內(nèi)核空間,內(nèi)核來調(diào)度,數(shù)量 M
- 用戶線程:運(yùn)行在用戶空間,線程庫來調(diào)度,數(shù)量 N
內(nèi)核線程相當(dāng)于用戶線程的容器,M <= N,根據(jù) M:N 取值將線程實(shí)現(xiàn)方式分為三種模式:完全在用戶空間實(shí)現(xiàn)、完全由內(nèi)核調(diào)度和雙層調(diào)度
Linux 線程庫:LinuxThreads 和 NPTL 線程庫
14.2 創(chuàng)建線程和結(jié)束線程
#include <pthread.h>
// othread_t 是 unsigned long int 類型
// attr 設(shè)置新線程的屬性
// start_routine 是函數(shù)指針,就是線程運(yùn)行的函數(shù),arg 是其參數(shù)
// [return] 成功返回 0,失敗返回錯(cuò)誤碼
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void *), void* arg);
// 通過 retval 參數(shù)向線程的回收者傳遞其退出信息
void pthread_exit(void* retval);
// thread 是目標(biāo)線程的標(biāo)識符
// retval 是目標(biāo)線程返回的退出信息
// [return] 成功返回 0,失敗返回錯(cuò)誤碼
int pthread_join(pthread_t thread, void** retval); // 會一直阻塞,知道被回收的線程結(jié)束
// thread 是目標(biāo)線程的標(biāo)識符
int pthread_cancel(pthread_t thread);
14.3 線程屬性
pthread_attr_t 結(jié)構(gòu)體定義了一套完整的線程屬性
#include <bits/pthreadtypes.h>
typedef union {
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
} pthread_attr_t;
// 初始化線程屬性對象
int pthread_attr_init(pthread_attr_t* attr);
// 銷毀線程屬性對象,被銷毀的線程屬性對象只有再次初始化之后才能繼續(xù)使用
int pthread_attr_destroy(pthread_attr_t* attr);
// 獲取和設(shè)置線程屬性對象的某個(gè)屬性很熟有很多...
14.4 POSIX 信號量
POSIX 信號量函數(shù)都以 sem_ 開頭
#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);
int sem_wait(sem_t* sem); // 信號量減1
int sem_trywait(sem_t* sem); // wait 的非阻塞版本
int sem_post(sem_t* sem); // 信號量加1
14.5 互斥鎖
基礎(chǔ) API
互斥鎖(也稱互斥量)可以用于保護(hù)關(guān)鍵代碼段,以確保其獨(dú)占式的訪向,這有點(diǎn)像一個(gè)二進(jìn)制信號量(見 13.5.1 小節(jié))。當(dāng)進(jìn)入關(guān)鍵代碼段時(shí),我們需要獲得互斥鎖并將其加鎖。這等價(jià)于二進(jìn)制信號量的P操作:當(dāng)離開關(guān)鍵代碼段時(shí),我們需要對互斥鎖解鎖,以喚醒其他等待該互斥鎖的線程,這等價(jià)于二進(jìn)制信號量的V操作。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexatrr_t* mutexattr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex); // 互斥鎖加鎖
int pthread_mutex_trylock(pthread_mutex_t* mutex); // lock 的非阻塞版本
int pthread_mutex_unlock(pthread_mutex_t* mutex); // 互斥鎖解鎖
互斥鎖屬性
pthread_mutexattr_t 結(jié)構(gòu)體定義了一套完整的互斥鎖屬性
#include <pthread.h>
/*初始化互斥鎖屬性對象*/
int pthread_mutexattr_init(pthread_mutexattr_t* attr);
/*銷毀互斥鎖屬性對象*/
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
/*獲取和設(shè)置互斥鎖的 pshared 屬性*/
int pthread_mutexattr_getpshared(const pthread_mutexattr_t* attr, int* pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t* attr, int pshared);
/*獲取和設(shè)置互斥鎖的 type 屬性*/
int pthread_mutexattr_gettype(const pthread_mutexattr_t* attr, int* type);
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);
- pshared 參數(shù)指定是否允許跨進(jìn)程共享互斥鎖
- type 參數(shù)指定互斥鎖類型:普通鎖、檢錯(cuò)鎖、嵌套鎖、默認(rèn)鎖
死鎖舉例
主線程獲取 mutex_a 之后等待 mutex_b,子線程獲取 mutex_b 之后等待 mutex_a
代碼參考:14-1mutual_lock.c
14.6 條件變量
條件變量用于在線程之間同步共享數(shù)據(jù)的值
#include <pthread.h>
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
int pthread_cond_destroy(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond); // 廣播方式喚醒所有等待目標(biāo)條件變量的線程
int pthread_cond_signal(pthread_cond_t* cond); // 喚醒一個(gè)等待目標(biāo)條件變量的線程
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
14.7 線程同步機(jī)制包裝類
將 sem、mutex 和 cond 封裝成類
代碼參考:14-2locker.h
14.8 多線程環(huán)境
可重入函數(shù)
一個(gè)函數(shù)能被多個(gè)線程同時(shí)調(diào)用且不發(fā)生競態(tài)條件,那就是線程安全的,該函數(shù)就是可重入函數(shù),Linux 庫函數(shù)中 inet_ntoa、getservbyname 等函數(shù)是不可重入的,主要是因?yàn)樗鼈儍?nèi)部使用了靜態(tài)變量。
不過 Linux 對很多不可重入的庫函數(shù)提供了對應(yīng)的可重入版本,原函數(shù)尾部加上 _r,多線程編程中一定要使用可重入版本
線程和進(jìn)程
思考這樣一個(gè)問題:如果一個(gè)多線程程序的某個(gè)線程調(diào)用了 fork 函數(shù),那么新創(chuàng)建的子進(jìn)程是否將自動創(chuàng)建和父進(jìn)程相同數(shù)量的線程呢?答案是 “否”,
正如我們期望的那樣。**子進(jìn)程只擁有一個(gè)執(zhí)行線程,它是調(diào)用 fork 的那個(gè)線程的完整復(fù)制。并且子進(jìn)程將自動繼承父進(jìn)程中互斥鎖(條件變量與之類似)的狀態(tài)。**也就是說,父進(jìn)程中已經(jīng)被加鎖的互斥鎖在子進(jìn)程中也是被鎖住的。
這就引起了一個(gè)問題:子進(jìn)程可能不清楚從父進(jìn)程繼承而來的互斥鎖的具體狀態(tài)(是加鎖狀態(tài)還是解鎖狀態(tài))。這個(gè)互斥鎖可能被加鎖了,但并不是由調(diào)用 fork 函數(shù)的那個(gè)線程鎖住的,而是由其他線程鎖住的。如果是這種情況,則子進(jìn)程若再次對該互斥鎖執(zhí)行加鎖操作就會導(dǎo)致死鎖,如代碼清單 14-3 所示。
代碼參考:14-3thread_atfork.c
線程與信號
多線程版本下的信號掩碼設(shè)置函數(shù)為
#include <pthread.h>
#include <signal.h>
int pthread_sigmask(int how, const sigset_t* newmask, sigset_t* oldmask);
由于進(jìn)程中的所有線程共享該進(jìn)程的信號,所以線程庫將根據(jù)線程掩碼決定把信號發(fā)送給哪個(gè)具體的線程。因此,如果我們在每個(gè)子線程中都單獨(dú)設(shè)登信號掩碼,就很容易導(dǎo)致邏輯錯(cuò)誤。此外,所有線程共享信號處理函數(shù)。也就是說,當(dāng)我們在一個(gè)線程中設(shè)置了某個(gè)信號的信號處理函數(shù)后,它將覆蓋其他線程為同一個(gè)信號設(shè)置的信號處理函數(shù)。這兩點(diǎn)都說明,我們應(yīng)該定義一個(gè)專門的線程來處理所有的信號。這可以通過如下兩個(gè)步驟來實(shí)現(xiàn):
-
在主線程創(chuàng)建出其他子線程之前就調(diào)用 pthread_sigmask 來設(shè)置好信號掩碼,所有新創(chuàng)建的子線程都將自動繼承這個(gè)信號掩碼。這樣做之后,實(shí)際上所有線程都不會響應(yīng)被屏蔽的信號了
-
在某個(gè)線程中調(diào)用如下函數(shù)來等待信號并處理之:
#include <signal.h> // set 參數(shù)指定需要等待的信號集合 // sig 存儲返回值 int sigwait(const sigset_t* set, int* sig); // 明確講一個(gè)信號 sig 發(fā)給 thread 線程,sig = 0 不發(fā)送信號 // thread 參數(shù)指定目標(biāo)線程,sig 指定待發(fā)送的信號 // 可以用于檢查指定線程是否存在,成功返回 0,失敗返回錯(cuò)誤碼 int pthread_kill(pthread_t thread, int sig);
代碼參考:14-5sigmask.c 沒看懂o(╥﹏╥)o
Ch.15 進(jìn)程池和線程池
15.1 概述
進(jìn)程池中的所有子進(jìn)程都運(yùn)行著相同的代碼,并具有相同的屬性,比如優(yōu)先級、PGID等。因?yàn)檫M(jìn)程池在服務(wù)器啟動之初就創(chuàng)建好了,所以每個(gè)子進(jìn)程都相對 “干凈”,即它們沒有打開不必要的文件描述符(從父進(jìn)程繼承而來),也不會錯(cuò)誤地使用大塊的堆內(nèi)存(從父進(jìn)程復(fù)制得到)。
當(dāng)有新的任務(wù)到來時(shí),主進(jìn)程將通過某種方式選擇進(jìn)程池中的某一個(gè)子進(jìn)程來為之服務(wù)。相比于動態(tài)創(chuàng)建子進(jìn)程,選擇一個(gè)已經(jīng)存在的子進(jìn)程的代價(jià)顯然要小得多。至于主進(jìn)程選擇哪個(gè)子進(jìn)程來為新任務(wù)服務(wù),則有兩種方式:
- 隨機(jī)算法和 Round Robin 算法、以及更優(yōu)秀的均勻分配算法
- 主進(jìn)程和所有子進(jìn)程通過一個(gè)共享的工作隊(duì)列來同步,子進(jìn)程都睡眠在該隊(duì)列上,有新任務(wù)時(shí)將任務(wù)添加到工作隊(duì)列,喚醒正在等待的子進(jìn)程
選擇好子進(jìn)程之后,可以通過管道實(shí)現(xiàn)父子進(jìn)程之間的數(shù)據(jù)傳遞
15.2 處理多客戶
- 半同步/半反應(yīng)堆:主進(jìn)程接受新的連接以得到連接 socket,然后它需要將該 socket 傳遞給子進(jìn)程(對于線程池而言,父線程將socket 傳遞給子線程必須使用13.9節(jié)介紹的 socketpair 系統(tǒng)調(diào)用創(chuàng)建的雙向管道實(shí)現(xiàn))
- 半同步/半異步模式以及領(lǐng)導(dǎo)者/追隨者模式,是由主進(jìn)程管理所有監(jiān)聽 socket,而各個(gè)子進(jìn)程分別管理屬于自己的連接 socket 的,子進(jìn)程自己調(diào)用 accept 來接受新的連接
15.3 半同步/半異步進(jìn)程池
為了避免在父子進(jìn)程之間傳遞文件描述符,我們將接收新連接的操作放到子進(jìn)程中
代碼參考:15-1processpool.h
15.4 用進(jìn)程池實(shí)現(xiàn)簡單的 CGI 服務(wù)器
復(fù)用前面的進(jìn)程池,構(gòu)建一個(gè)并發(fā)的 CGI 服務(wù)器
代碼參考:15-2pool_cgi.cpp
15.5 半同步/半反應(yīng)堆線程次
本節(jié)我們實(shí)現(xiàn)一個(gè)基于圖 8-10所示的半同步/半反應(yīng)堆并發(fā)模式的線程池,如代碼清單15-3所示。相比代碼清單 15-1 所示的進(jìn)程池實(shí)現(xiàn),該線程池的通用性要高得多,因?yàn)樗褂靡粋€(gè)工作隊(duì)列完全解除了主線程和工作線程的耦合關(guān)系:主線程往工作隊(duì)列中插入任務(wù),工作線程通過競爭來取得任務(wù)并執(zhí)行它。不過,如果要將該線程池應(yīng)用到實(shí)際服務(wù)器程序中,那么我們必須保證所有客戶請求都是無狀態(tài)的,因?yàn)橥粋€(gè)連接上的不同請求可能會由不同的線程處理。
代碼參考:15-3threadpool.h文章來源:http://www.zghlxwxcb.cn/news/detail-458535.html
15.6 用線程池實(shí)現(xiàn)的簡單 web 服務(wù)器
- 15-4http_conn.h
- 15-5http_conn.cpp
- 15-6main.cpp
類似于一個(gè)傳輸文本服務(wù)器,簡單測試的客戶端可以使用 telnet文章來源地址http://www.zghlxwxcb.cn/news/detail-458535.html
# HTTP 請求格式
GET /test.txt HTTP/1.1
Connection: keep-alive
Content-Length: 8
Host: telnet
param1=1
到了這里,關(guān)于【閱讀筆記】Linux 高性能服務(wù)器編程的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!