国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【閱讀筆記】Linux 高性能服務(wù)器編程

這篇具有很好參考價(jià)值的文章主要介紹了【閱讀筆記】Linux 高性能服務(wù)器編程。希望對大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

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):

  1. 在主線程創(chuàng)建出其他子線程之前就調(diào)用 pthread_sigmask 來設(shè)置好信號掩碼,所有新創(chuàng)建的子線程都將自動繼承這個(gè)信號掩碼。這樣做之后,實(shí)際上所有線程都不會響應(yīng)被屏蔽的信號了

  2. 在某個(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

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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 《Linux高性能服務(wù)器編程》筆記01

    《Linux高性能服務(wù)器編程》筆記01

    本文是讀書筆記,如有侵權(quán),請聯(lián)系刪除。 參考 Linux高性能服務(wù)器編程源碼: https://github.com/raichen/LinuxServerCodes 豆瓣: Linux高性能服務(wù)器編程 □socket地址API。socket最開始的含義是一個(gè)IP地址和端口對(ip,port)。它唯一地 表示了使用TCP通信的一端。本書稱其為socket地址。 □s

    2024年01月22日
    瀏覽(43)
  • Linux高性能服務(wù)器編程——學(xué)習(xí)筆記①

    Linux高性能服務(wù)器編程——學(xué)習(xí)筆記①

    第一章有一些概念講的很好,值得好好關(guān)注一下?。。?1.1 主要的協(xié)議 1.1.1 數(shù)據(jù)鏈路層 ? 數(shù)據(jù)鏈路層實(shí)現(xiàn)了網(wǎng)卡接口的網(wǎng)絡(luò)驅(qū)動程序,以處理數(shù)據(jù)在物理媒介(以太網(wǎng)、令牌環(huán))上的傳輸。 ? 常用的協(xié)議有兩種: ARP協(xié)議(Address Resolve Protocol,地址解析協(xié)議) RARP(Reverse

    2024年01月20日
    瀏覽(35)
  • Linux高性能服務(wù)器編程——ch10筆記

    信號是由用戶、系統(tǒng)或者進(jìn)程發(fā)送給目標(biāo)進(jìn)程的信息,以通知目標(biāo)進(jìn)程某個(gè)狀態(tài)的改變或系統(tǒng)異常。 :::tips int kill(pid_t pid, int sig); ::: kill函數(shù):一個(gè)進(jìn)程給其他進(jìn)程發(fā)送信號的API。 sig一般大于0,如果設(shè)為0則表示不發(fā)送信號,可以用來檢測進(jìn)程或進(jìn)程組是否存在。由于進(jìn)程P

    2024年02月06日
    瀏覽(25)
  • Linux高性能服務(wù)器編程 學(xué)習(xí)筆記 第二章 IP協(xié)議詳解

    Linux高性能服務(wù)器編程 學(xué)習(xí)筆記 第二章 IP協(xié)議詳解

    本章從兩方面探討IP協(xié)議: 1.IP頭部信息。IP頭部出現(xiàn)在每個(gè)IP數(shù)據(jù)報(bào)中,用于指定IP通信的源端IP地址、目的端IP地址,指導(dǎo)IP分片和重組,指定部分通信行為。 2.IP數(shù)據(jù)報(bào)的路由和轉(zhuǎn)發(fā)。IP數(shù)據(jù)報(bào)的路由和轉(zhuǎn)發(fā)發(fā)生在除目標(biāo)機(jī)器外的所有主機(jī)和路由器上,它們決定數(shù)據(jù)報(bào)是否應(yīng)

    2024年02月09日
    瀏覽(31)
  • Linux高性能服務(wù)器編程 學(xué)習(xí)筆記 第五章 Linux網(wǎng)絡(luò)編程基礎(chǔ)API

    Linux高性能服務(wù)器編程 學(xué)習(xí)筆記 第五章 Linux網(wǎng)絡(luò)編程基礎(chǔ)API

    我們將從以下3方面討論Linux網(wǎng)絡(luò)API: 1.socket地址API。socket最開始的含義是一個(gè)IP地址和端口對(ip,port),它唯一表示了使用TCP通信的一端,本書稱其為socket地址。 2.socket基礎(chǔ)API。socket的主要API都定義在sys/socket.h頭文件中,包括創(chuàng)建socket、命名socket、監(jiān)聽socket、接受連接、發(fā)

    2024年02月07日
    瀏覽(41)
  • Linux高性能服務(wù)器編程 學(xué)習(xí)筆記 第一章 TCP/IP協(xié)議族

    Linux高性能服務(wù)器編程 學(xué)習(xí)筆記 第一章 TCP/IP協(xié)議族

    現(xiàn)在Internet使用的主流協(xié)議族是TCP/IP協(xié)議族,它是一個(gè)分層、多協(xié)議的通信體系。 TCP/IP協(xié)議族包含眾多協(xié)議,我們只詳細(xì)討論IP協(xié)議和TCP協(xié)議,因?yàn)樗鼈儗帉懢W(wǎng)絡(luò)應(yīng)用程序有最直接的影響。如果想系統(tǒng)學(xué)習(xí)網(wǎng)絡(luò)協(xié)議,RFC(Request For Comments,評論請求)是首選資料。 TCP/IP協(xié)議

    2024年02月09日
    瀏覽(41)
  • 【Linux高性能服務(wù)器編程】——高性能服務(wù)器框架

    【Linux高性能服務(wù)器編程】——高性能服務(wù)器框架

    ? hello !大家好呀! 歡迎大家來到我的Linux高性能服務(wù)器編程系列之高性能服務(wù)器框架介紹,在這篇文章中, 你將會學(xué)習(xí)到高效的創(chuàng)建自己的高性能服務(wù)器,并且我會給出源碼進(jìn)行剖析,以及手繪UML圖來幫助大家來理解,希望能讓大家更能了解網(wǎng)絡(luò)編程技術(shù)?。?! 希望這篇

    2024年04月25日
    瀏覽(101)
  • Linux高性能服務(wù)器——狀態(tài)機(jī)

    有的應(yīng)用層協(xié)議頭部包含數(shù)據(jù)包類型字段,每種類型可以映射為邏輯單元的一種執(zhí)行狀態(tài),服務(wù)器可以根據(jù)它來編寫相應(yīng)的處理邏輯。 狀態(tài)之間的轉(zhuǎn)移是需要狀態(tài)機(jī)內(nèi)部驅(qū)動的。 TCP / IP 協(xié)議都在其頭部中提供頭部長度字段。程序根據(jù)該字段的值就可以知道是否接收到一個(gè)完

    2024年02月08日
    瀏覽(31)
  • 讀高性能MySQL(第4版)筆記05_優(yōu)化服務(wù)器設(shè)置

    讀高性能MySQL(第4版)筆記05_優(yōu)化服務(wù)器設(shè)置

    2.1.3.1.?MySQL只需要少量的內(nèi)存就能保持一個(gè)連接(通常是一個(gè)相關(guān)的專用線程)打開 2.2.1.1.?InnoDB緩沖池大小 2.2.1.2.?需要的內(nèi)存比其他任何組件都多 2.2.1.3.?不僅緩存索引,還緩存行數(shù)據(jù)、自適應(yīng)哈希索引、更改緩沖區(qū)、鎖和其他內(nèi)部結(jié)構(gòu)等 2.2.1.4.?InnoDB嚴(yán)重依賴緩沖池,應(yīng)

    2024年02月09日
    瀏覽(33)
  • linux系統(tǒng)下如何使用nginx作為高性能web服務(wù)器

    linux系統(tǒng)下如何使用nginx作為高性能web服務(wù)器

    ?? 歡迎大家來到景天科技苑?? ???? 養(yǎng)成好習(xí)慣,先贊后看哦~???? ?? 作者簡介:景天科技苑 ??《頭銜》:大廠架構(gòu)師,華為云開發(fā)者社區(qū)專家博主,阿里云開發(fā)者社區(qū)專家博主,CSDN新星創(chuàng)作者,掘金優(yōu)秀博主,51CTO博客專家等。 ??《博客》:Python全棧,前后端開

    2024年04月14日
    瀏覽(37)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包