今兒我們就從源碼入手,來(lái)幫助大家簡(jiǎn)單理解一下 epoll 的實(shí)現(xiàn)原理,并在后邊分析一下,大家都說(shuō) epoll 性能好,那到底是好在哪里。
epoll 簡(jiǎn)介
1、epoll 的簡(jiǎn)單使用
我們先來(lái)看下 epoll 的簡(jiǎn)單使用。
首先來(lái)看下不用 epoll 的時(shí)候,我們可能會(huì)怎樣去創(chuàng)建一個(gè) socket 鏈接的偽代碼:
socket_fd = socket(AF_INET,SOCK_STREAM,0);
// 給 socket 綁定本地端口和地址
local_addr.sin_port = htons(PORT);
local_addr.sin_addr.s_addr = INADDR_ANY;
ret = bind(socket_fd,(struct sockaddr*)&local_addr,sizeof(struct sockaddr_in));
// 監(jiān)聽客戶端發(fā)來(lái)的鏈接
ret = listen(socket_fd,backlog);
// 死循環(huán)
for(;;){
// 當(dāng)用戶調(diào)用了 connect 后服務(wù)端會(huì)觸發(fā) accept
accept_fd = accept( socket_fd, (struct sockaddr *)&remote_addr, &addr_len );
for(;;){
// 從線程池里撈一條線程然后把這個(gè) accept 交給這條線程
// 然后線程中去做 recv()
get_thread_from_pool(accept_fd)
}
}
不同語(yǔ)言可能寫法都不太一樣,但是大概流程都是先創(chuàng)建個(gè) socket,然后給 socket 綁定上本地端口和 ip,以便客戶端能通過(guò)這倆信息找到自己,之后監(jiān)聽這個(gè) socket,再然后死循環(huán)中用 accept 來(lái)接受用戶的 connect,接收到之后,把鏈接的 fd 扔給一條新的線程中去做 read 之類的操作。
我們?cè)賮?lái)簡(jiǎn)單看下用 epoll 的時(shí)候大概會(huì)怎么寫:
// 創(chuàng)建 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 給 socket 綁定地址和 port 并監(jiān)聽
myaddr.sin_port = htons(PORT);
myaddr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (const struct sockaddr *)&myaddr, sizeof(myaddr))
listen(sockfd)
// 創(chuàng)建 epoll
int efd = epoll_create(1);
// 創(chuàng)建 epoll 的事件
struct epoll_event evt = {
.events = EPOLLIN,
.data.fd = sockfd,
};
// 把 socket 交給 epoll 做托管
epoll_ctl(efd, EPOLL_CTL_ADD, sockfd, &evt)
struct epoll_event events[MAX];
while (1) {
// 觸發(fā) epoll 的等待, 等用戶的 connect 以及 send
int num = epoll_wait(efd, events);
for (i = 0; i < num; i++) {
if (events[i].events & EPOLLIN) {
// 如果是 socketfd 收到了 connect
if (sockfd == events[i].data.fd) {
// 就把這條鏈接的 fd 也放到 epoll 中
int cn_fd = accept(sockfd, NULL, NULL);
struct epoll_event ac_evt = {
.events = EPOLLIN,
.data.fd = cn_fd,
};
epoll_ctl(efd, EPOLL_CTL_ADD, cn_fd, &ac_evt);
} else {
// 如果是收到了用戶的 send, 那就從線程池里撈出一條線程
// 然后里頭再去做 read 之類的操作
get_thread_from_pool(events[i].data.fd);
}
}
}
}
}
上邊的代碼簡(jiǎn)單來(lái)講也是先創(chuàng)建 socket,然后創(chuàng)建 epoll,之后將 socket 交給 epoll 管理,隨后啟動(dòng)死循環(huán),當(dāng)用戶 connect 了之后再把這個(gè) accept 的 fd 同樣托管給 epoll,這樣當(dāng)用戶發(fā)消息過(guò)來(lái)之后就會(huì)從線程池中撈一條線程,然后用這條線程去做 read 之類的操作。
用以及不用 epoll 大概就是上邊這兩種情況,這里都是偽代碼,具體一點(diǎn)的代碼可以很容易搜到,大家如果想自己試的話可以去搜一搜,這里就簡(jiǎn)單帶過(guò)了。
2、epoll 的系統(tǒng)調(diào)用
epoll 主要有仨系統(tǒng)調(diào)用:
- epoll_create: 創(chuàng)建一個(gè) epoll 對(duì)象
- epoll_ctl: 把要管理的對(duì)象添加到 epoll 中
- epoll_wait: hang 住當(dāng)前線程等待被托管的東西里有 IO 發(fā)生
epoll 實(shí)現(xiàn)原理
epoll 的實(shí)現(xiàn)原理可能會(huì)有點(diǎn)繞,如果不想看中間那大坨源代碼的話,大家可以直接跳到后邊 “幾個(gè)系統(tǒng)調(diào)用總結(jié)” 這部分來(lái)看最后的總結(jié)。
1、epoll 是文件系統(tǒng)
首先 epoll 深得 unix 設(shè)計(jì)哲學(xué)的精髓,他也和 socket 一樣,是個(gè)文件系統(tǒng),它的主要系統(tǒng)調(diào)用實(shí)現(xiàn)在內(nèi)核源碼的 “fs/eventpoll.c” 文件中。
在之前的文章中介紹過(guò) Linux 的文件系統(tǒng)以及 sockfs,并且當(dāng)時(shí)提到文件系統(tǒng)有基于磁盤的,也有基于內(nèi)存的。當(dāng)時(shí)介紹的 sockfs 就是基于內(nèi)存的文件系統(tǒng)。很明顯,這里的 epoll 文件系統(tǒng)也是基于內(nèi)存的一種文件系統(tǒng)。
我們?cè)谥暗奈恼轮刑岬?,?duì)于基于磁盤的文件系統(tǒng)比如 ext4 等他們都在內(nèi)存中有自己的 inode 數(shù)據(jù)結(jié)構(gòu),這個(gè) inode 數(shù)據(jù)結(jié)構(gòu)上保存了很多對(duì)當(dāng)前文件系統(tǒng)的操作方法以及屬性。然后用戶態(tài)在使用的時(shí)候,大概就是在線程的 task_struct 結(jié)構(gòu)體上找到 files 屬性中的 fd_array 或者 fd_table,然后通過(guò) fd 找到對(duì)應(yīng)的 file 結(jié)構(gòu)體,之后通過(guò) file 結(jié)構(gòu)體,就能找到對(duì)應(yīng)的 inode 然后做一些文件相關(guān)的操作。
而對(duì)于類似 sockfs 或者 epoll 這種基于內(nèi)存的文件系統(tǒng)來(lái)講,他們雖然也有 inode 屬性,但對(duì)他們來(lái)講,這個(gè) inode 是一種 “假的” inode,也就是說(shuō)對(duì)于 epoll 來(lái)講,它的 inode 作用不大,而真正有用的,是掛載在 file 結(jié)構(gòu)體上的 private_data 屬性,這點(diǎn)它和 socket 一樣。
到這兒為止,如果感覺不是很清晰的話,可以去看下之前介紹 sockfs 的文章,或者也可以簡(jiǎn)單地記,就是:
- epoll 和 socket 一樣也是一種文件系統(tǒng)
- 當(dāng)用戶調(diào)用了 epoll_create 之后會(huì)返回 epoll 的 fd
- 通過(guò)這個(gè) fd,可以在 task_struct 的 files 上找到對(duì)應(yīng)的 epoll 的 file 結(jié)構(gòu)體
- 在這個(gè) file 結(jié)構(gòu)體上可以拿到一個(gè) private_data 屬性,這個(gè) private_data 屬性的值,就是 epoll 內(nèi)核中的數(shù)據(jù)結(jié)構(gòu)。至于這個(gè)結(jié)構(gòu)是什么東西,咱們后邊再說(shuō)。
2、epoll_create
首先我們來(lái)分析一下想使用 epoll 的話,一定要走的第一個(gè)系統(tǒng)調(diào)用 “epoll_create”。
上圖是源碼中的實(shí)現(xiàn),我們來(lái)簡(jiǎn)單看下:
int error, fd;
struct eventpoll *ep = NULL;
struct file *file;
// 創(chuàng)建了一個(gè) eventpoll 結(jié)構(gòu)體
error = ep_alloc(&ep);
// 生成文件描述符
fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
// 創(chuàng)建 epoll 對(duì)應(yīng)的 file 結(jié)構(gòu)體
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
ep->file = file;
// 給它綁上 fd
fd_install(fd, file);
return fd;
}
上邊是把這個(gè)系統(tǒng)調(diào)用最外層的精華摘了出來(lái),主要做的事兒,就是創(chuàng)建了一個(gè) eventpoll 結(jié)構(gòu)體,咱們上一小節(jié)說(shuō)的通過(guò) fd 找到 file 然后找到的那個(gè) private_data 屬性,其實(shí)就是這個(gè) eventpoll 結(jié)構(gòu)體。
通過(guò)上邊源碼可以看到,這個(gè) eventpoll 結(jié)構(gòu)體作為 priv 參數(shù)交給了 “file->private_data” 方法。另外在源代碼中也可以看到,epoll 對(duì)應(yīng)的這個(gè) file 結(jié)構(gòu)體,是用一個(gè)叫 “alloc_file_pseudo” 的方法創(chuàng)建的,其中這個(gè) “pseudo” 是 “假的” 的意思,這也表明了對(duì)于 epoll 這種基于內(nèi)存的文件系統(tǒng),它的 file 結(jié)構(gòu)體相比基于磁盤的文件系統(tǒng)沒(méi)有那么 “沉”。
接下來(lái)我們回到上邊創(chuàng)建完了 eventpoll 結(jié)構(gòu)體之后,epoll_create 系統(tǒng)調(diào)用中會(huì)獲取一個(gè)未使用的文件描述符,然后給 epoll 創(chuàng)建一個(gè) file 結(jié)構(gòu)體,并把這個(gè) file 結(jié)構(gòu)體和 fd 做一個(gè) “fd_install”,也就是給綁定一下子,這樣通過(guò)這個(gè) fd 就能在當(dāng)前線程的 task_struct 上找到對(duì)應(yīng)的這個(gè) eventpoll 數(shù)據(jù)結(jié)構(gòu)了。
上邊我們反復(fù)提到 eventpoll 這個(gè)結(jié)構(gòu)體,從 epoll_create 系統(tǒng)調(diào)用的源碼也能看出,這個(gè)系統(tǒng)調(diào)用主要就是創(chuàng)建出了這個(gè)一個(gè)結(jié)構(gòu)體,并且能讓我們通過(guò) fd 找到他,那他到底是個(gè)啥呢?我們來(lái)下源碼:
這個(gè) eventpoll 結(jié)構(gòu)體上有很多屬性,其中最重要的,我們只需要記住三個(gè)就好:
- wq: 一個(gè)存放等待事件的隊(duì)列
- rdllist: 一個(gè)存放就緒事件的隊(duì)列
- rbr: 一顆紅黑樹
至于這仨分別是干啥的,一會(huì)兒在后邊的文章中就能看到了。
這里簡(jiǎn)單總結(jié)一下,使用 epoll 的第一步!調(diào)用 epoll_create 方法,該方法做的事情就是創(chuàng)建了一個(gè) eventpoll 結(jié)構(gòu)體,并且能讓用戶態(tài)通過(guò) fd 找到這個(gè) eventpoll 結(jié)構(gòu)體。這個(gè)結(jié)構(gòu)體上重點(diǎn)有仨屬性,一個(gè)用來(lái)存放等待事件的隊(duì)列,一個(gè)用來(lái)存放就緒事件的隊(duì)列,以及一顆紅黑樹。
2、epoll_ctl
接下來(lái)我們來(lái)看使用 epoll 的第二步,使用 epoll_ctl 系統(tǒng)調(diào)用,將要托管的 socket fd 交給 epoll 托管。代碼大概長(zhǎng)這樣:
這一步就是將 socket 交給 epoll 管理,我們來(lái)簡(jiǎn)單介紹下它里頭做了什么事兒,這里可能有些邏輯會(huì)比較繞,大家可以自己再去看看源碼加深一下理解:
struct eventpoll *ep;
// 根據(jù) epoll 的 fd 找到對(duì)應(yīng)的 eventpoll 的 file 結(jié)構(gòu)體
f = fdget(epfd);
// 根據(jù) socket 的 fd 找到對(duì)應(yīng)的 socket 的 file 結(jié)構(gòu)體
tf = fdget(fd);
// 檢查是否支持 poll
if (!file_can_poll(tf.file))
goto error_tgt_fput;
// 找到對(duì)應(yīng)的 eventpoll 結(jié)構(gòu)體
ep = f.file->private_data;
switch (op) {
// 添加一個(gè) socket 到 epoll 中
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= EPOLLERR | EPOLLHUP;
error = ep_insert(ep, &epds, tf.file, fd, full_check);
}
break;
case EPOLL_CTL_DEL:
case EPOLL_CTL_MOD:
}
}
上邊是 epoll_ctl 這個(gè)系統(tǒng)調(diào)用的主要代碼,里頭做的事情乍一看也很簡(jiǎn)單:
- 根據(jù) epoll 的 fd 找到對(duì)應(yīng)的 file 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體上能找到 eventpoll 結(jié)構(gòu)體
- 根據(jù) socket 的 fd 找到對(duì)應(yīng)的 file 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體上能找到 socket 結(jié)構(gòu)體
- 調(diào)用了 ep_insert 方法,將 socket 插入到 eventpoll 結(jié)構(gòu)體中
下面我們來(lái)看看 “ep_insert” 這個(gè)方法做了啥:
{
// 初始化一個(gè) epitem 數(shù)據(jù)結(jié)構(gòu)
struct epitem *epi;
// 初始化一個(gè)等待隊(duì)列,但它其實(shí)是個(gè) struct 結(jié)構(gòu)體
// 上邊只有一個(gè) poll_table 結(jié)構(gòu)體和 epitem 結(jié)構(gòu)體
struct ep_pqueue epq;
// 初始化 epitem 結(jié)構(gòu)上的 pwqlist 屬性
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
// 這里只做了 ffd->file = file 以及 ffd->fd = fd
ep_set_ffd(&epi->ffd, tfile, fd);
// 給等待隊(duì)列的 epitem 賦值
epq.epi = epi;
// 給等待隊(duì)列的 poll_table 賦值
// 賦的值可以簡(jiǎn)單地認(rèn)為就是后邊這個(gè)叫做 “ep_ptable_queue_proc” 的函數(shù)
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
// 這里會(huì)調(diào)用上邊那行的 “ep_ptable_queue_proc” 方法
// 作用可以簡(jiǎn)單理解成給 epi 的 pwqlist 這條鏈表上
// 添加了一個(gè)叫做 “ep_poll_callback” 的回調(diào)函數(shù)
revents = ep_item_poll(epi, &epq.pt, 1);
// 把這個(gè) epitem 插入到 eventpoll 的紅黑樹里
ep_rbtree_insert(ep, epi);
}
這個(gè) ep_insert 方法中,從宏觀上來(lái)看,主要做的事情就是創(chuàng)建了一個(gè)紅黑樹的節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)上保存了用戶傳進(jìn)來(lái)的 socket 相關(guān)的信息,然后主要還調(diào)用了一個(gè) ep_item_poll 用來(lái)初始化等待隊(duì)列。
這里最最最主要的操作其實(shí)就是這個(gè) “ep_item_poll” 方法了,這個(gè)方法主要是往 epi 的 pwqlist 這條鏈表上掛了個(gè)回調(diào)函數(shù)名字叫 “ep_poll_callback”,那么這個(gè) epi 的 “pwqlist” 又是誰(shuí),是干嘛的呢?這里我們直接揭秘,其實(shí)這個(gè) “pwqlist” 就是用戶傳進(jìn)來(lái)的那個(gè) socket 身上的 “等待隊(duì)列”。我們來(lái)詳細(xì)看下源碼,這里會(huì)比較繞,我盡量說(shuō)得簡(jiǎn)單點(diǎn):
pt->_key = epi->event.events;
if (!is_file_epoll(epi->ffd.file))
return vfs_poll(epi->ffd.file, pt) & epi->event.events;
}
static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt) {
return file->f_op->poll(file, pt);
}
雖然我們上邊說(shuō)這塊兒會(huì)比較繞,但實(shí)際上這個(gè) “ep_item_poll” 的源碼還算是比較短的,我們把最重要的摘出來(lái)其實(shí)就這么幾行,可以看到它里頭調(diào)用了 vfs_poll 方法,vfs_poll 方法中又去調(diào)用了 file -> f_op -> poll 方法。
從這里我們就可以看出,如果你的文件系統(tǒng)實(shí)現(xiàn)了 poll 方法的話,其實(shí)理論上是都可以被 epoll 來(lái)托管的。那么這里這個(gè) poll 方法是誰(shuí)呢?這里不賣關(guān)子直接說(shuō),其實(shí)這個(gè) file -> f_op -> poll 方法就是 tcp 協(xié)議自己實(shí)現(xiàn)的 poll 方法,也就是 “tcp_poll” 方法。
這里簡(jiǎn)單解釋一下這個(gè) tcp_poll 方法是怎么來(lái)的:首先大家都知道 socket 這個(gè)東西,但其實(shí) socket 之下還有更重要的一個(gè)叫做 “sock” 的結(jié)構(gòu)。對(duì)于這個(gè) socket 和 sock 應(yīng)該怎么理解呢?其實(shí)可以把 socket 理解成 “協(xié)議簇”,把 sock 理解為真正的 “協(xié)議”,socket 是用戶層的概念,而 sock 則是真的要和一種底層的協(xié)議做綁定的,比如 tcp 協(xié)議或者 udp 協(xié)議。然后不同的協(xié)議實(shí)現(xiàn)的什么 read 方法,send 方法,poll 方法等,就會(huì)被掛載到這個(gè) sock 結(jié)構(gòu)體上,也就是說(shuō),當(dāng)用戶在用戶側(cè)調(diào)用了一個(gè)什么 send 方法或者 recv 方法啥的,真正的調(diào)用邏輯是 “socket -> sock -> ops -> tcp_recv(或者 udp_recv)”。所以上邊的 ep_item_poll 方法里頭調(diào)用的 poll 方法,就是 socket -> sock -> ops -> tcp_poll 方法。
也就是說(shuō),這里可以簡(jiǎn)單地理解一下,當(dāng)用戶態(tài)調(diào)用了 “epoll_ctl” 并把一個(gè) socket 傳進(jìn)來(lái)的時(shí)候,這個(gè)系統(tǒng)調(diào)用會(huì)調(diào)用 socket 下層的 poll 接口,而實(shí)現(xiàn)了這個(gè) poll 接口的,就是下層真正的協(xié)議,比如 tcp 協(xié)議,此時(shí)就會(huì)調(diào)用 tcp 協(xié)議自己實(shí)現(xiàn)的 tcp_poll 方法。
好了回過(guò)頭繼續(xù)看這個(gè) tcp_poll 方法,注意 “ep_item_poll” 在調(diào)用這個(gè) tcp_poll 方法的時(shí)候,把一個(gè) “poll_table” 類型的屬性作為參數(shù)傳給了 tcp_poll,這個(gè) poll_table 是誰(shuí)呢,我們暫時(shí)回頭去看下 “ep_insert” 方法中的那個(gè) “init_poll_funcptr” 方法:
可以看到 init_poll_funcptr 接收了一個(gè) “ep_ptable_queue_proc” 方法,并把這個(gè)方法放到 “poll_table” 這個(gè)結(jié)構(gòu)體的 “_qproc” 屬性上,這里大家先強(qiáng)行記住這個(gè) “_qproc”,記住這個(gè) poll_table 結(jié)構(gòu)體上有個(gè) _qproc 屬性,并且指向了一個(gè)叫 “ep_ptable_queue_proc” 的函數(shù)。
然后我們往下看,上邊說(shuō)到 “ep_item_poll” 會(huì)調(diào)用 tcp 協(xié)議實(shí)現(xiàn)的 poll 方法并把這個(gè) poll_table 作為參數(shù)傳進(jìn)去,那我們來(lái)看看 tcp_poll 中實(shí)現(xiàn)了啥:
我們順著 tcp_poll 的這條調(diào)用鏈看下去,最終在 poll_wait 中看到了一個(gè)眼熟的東西,誒!就是上邊讓大家記住的那個(gè) “_qproc” 屬性,它指向了 “ep_ptable_queue_proc” 方法。
嗷!到這兒我們能反應(yīng)過(guò)來(lái),在 epoll_ctl 這個(gè)系統(tǒng)調(diào)用中,調(diào)用了底層協(xié)議的 poll 方法,并且把 epoll 那層的一個(gè)函數(shù)作為參數(shù)傳給了底層協(xié)議的 poll 方法,然后底層協(xié)議的 poll 方法又會(huì)調(diào)用這個(gè)函數(shù)。
是不是覺得有點(diǎn)繞了,還有更繞的。
我們來(lái)看上圖那個(gè) poll_wait 函數(shù),你看它調(diào)用 _qproc 方法時(shí)候傳的參數(shù)是個(gè)誰(shuí)?其中是不是有個(gè)叫做 “wait_address” 的東西,然后您再往上看上一張圖的 “sock_poll_wait” 方法,在調(diào)用這個(gè) “poll_wait” 方法時(shí),傳進(jìn)來(lái)的這個(gè) “wait_address” 是誰(shuí)呢?沒(méi)錯(cuò),正是 socket 的 wq.wait,也就是 socket 上的一個(gè)等待隊(duì)列。
好,到這兒我們?cè)谑崂硪幌铝鞒?,?dāng)用戶調(diào)用 “epoll_ctl” 并傳進(jìn)來(lái)一個(gè) socketfd 的時(shí)候,epoll ctl 內(nèi)部會(huì)調(diào)用這個(gè) socket 底層的協(xié)議實(shí)現(xiàn)的 poll 方法,并把自己的一個(gè) poll_table 屬性傳進(jìn)去,然后在底層協(xié)議比如 tcp 協(xié)議實(shí)現(xiàn)的 poll 方法中,又會(huì)調(diào)用上層的 epoll_ctl 傳進(jìn)來(lái)的這個(gè) poll_table 上的 _qproc 方法,并把自己這個(gè) socket 身上的等待隊(duì)列作為參數(shù)傳給這個(gè) _qproc 方法,而這個(gè) _qproc 方法指向的是 “ep_ptable_queue_proc” 這個(gè)函數(shù)。所以接下來(lái)我們來(lái)看 “ep_ptable_queue_proc” 方法:
struct file *file,
wait_queue_head_t *whead,
poll_table *pt
) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
add_wait_queue(whead, &pwq->wait);
}
init_waitqueue_func_entry(struct wait_queue_entry *wq_entry, wait_queue_func_t func)
{
wq_entry->flags = 0;
wq_entry->private = NULL;
wq_entry->func = func;
}
我們撿主要的看,這個(gè)函數(shù)中,最重要的兩個(gè)步驟就是首先是調(diào)用 “init_waitqueue_func_entry” 方法,這個(gè)方法很簡(jiǎn)單,直接貼在上邊了,就是把 “ep_poll_callback” 這個(gè)方法給掛到 pwp->wait 上邊。接下來(lái)調(diào)用 “add_wait_queue”,把這個(gè)掛載了 “ep_poll_callback” 方法的 pwp->wait 結(jié)構(gòu)給掛載到 whead 這個(gè)隊(duì)列上,那這個(gè) whead 是誰(shuí)呢,你一定能想到,就是上邊 tcp_poll 在調(diào)用這個(gè) “ep_ptable_queue_proc” 方法時(shí)傳進(jìn)來(lái)的 socket 自己身上的 wq 等待隊(duì)列。
到這兒,我們總結(jié)一下 epoll_ctl 都做了啥:
- 在 epoll_ctl 中調(diào)用了傳進(jìn)來(lái)的那個(gè) socket 底層協(xié)議的 poll 方法,比如底層協(xié)議如果是 tcp 的話,那這個(gè)方法就是 tcp_poll
- epoll_ctl 在調(diào)用 tcp_poll 時(shí),把自己這邊的一個(gè)回調(diào)函數(shù)傳給了 tcp_poll
- tcp_poll 中又會(huì)調(diào)用上層 epoll_ctl 傳給他的這個(gè)回調(diào)函數(shù),并且 tcp_poll 把自己的 socket 身上的等待隊(duì)列作為參數(shù)傳給這個(gè) epoll_ctl 傳下來(lái)的回調(diào)函數(shù)
- 這個(gè) epoll_ctl 中的會(huì)調(diào)用拿到了底層協(xié)議自己的 wq 等待隊(duì)列后,往這個(gè)等待隊(duì)列中推入了一個(gè)數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)中只有一個(gè)回調(diào)函數(shù),叫 “eo_poll_callback”
- 最后把這個(gè) socket 插到 epoll 內(nèi)部的紅黑樹上
好了到這兒我們就把 epoll_ctl 主要做的事兒都說(shuō)完了??梢园l(fā)現(xiàn)這套流程如果要是自己一點(diǎn)點(diǎn)看的話,確實(shí)會(huì)比較繞,因?yàn)樗镞呄喈?dāng)于是上層的 epoll 和下層的協(xié)議都是可以替換的,只要下層協(xié)議實(shí)現(xiàn)了 poll 方法,然后上層能把自己的回調(diào)注入進(jìn)入,之后下層的 poll 方法再把自己的等待隊(duì)列注入給上層的回調(diào)函數(shù),這就 ok 了,有一種雙向依賴注入的感覺。還挺(má)妙(fán)的是吧。
3、epoll_wait
說(shuō)完了 epoll_create 和 epoll_ctl 我們來(lái)看是用 epoll 的最后一個(gè)重要的系統(tǒng)調(diào)用 “epoll_wait”。
epoll_wait 主要調(diào)用的是 “do_epoll_wait” 中的 “ep_poll” 方法,我們來(lái)看一下:
int maxevents, long timeout)
{
// 先判斷 eventpoll 上的就緒隊(duì)列是不是有東西
// 有的話直接吐給用戶
if (!ep_events_available(ep))
ep_busy_loop(ep, timed_out);
eavail = ep_events_available(ep);
if (eavail)
goto send_events;
// 使用 current 初始化一個(gè)等待項(xiàng)
init_waitqueue_entry(&wait, current);
// 把等待項(xiàng)給干到 eventpoll 結(jié)構(gòu)體的 wq 隊(duì)列上
__add_wait_queue_exclusive(&ep->wq, &wait);
for (;;) {
// hang 住當(dāng)前線程
set_current_state(TASK_INTERRUPTIBLE);
}
}
對(duì)于這個(gè) “ep_poll” 整體上來(lái)看,做的事情比較直觀,主要就是:
- 先看看 epoll 里頭的就緒隊(duì)列是不是已經(jīng)有東西了。還記得最開始我們介紹 eventpoll 時(shí)候說(shuō)它里頭有三個(gè)重要的東西,一個(gè)是 “就緒隊(duì)列”,一個(gè)是 “等待隊(duì)列”,還有一顆 “紅黑樹”,此時(shí)看的就是這個(gè) eventpoll 中的 “就緒隊(duì)列”。
- 就緒隊(duì)列里沒(méi)東西的話會(huì)創(chuàng)建一個(gè)所謂的 “等待項(xiàng)”,這個(gè)是啥呢,后邊再說(shuō)。
- 創(chuàng)建好等待項(xiàng)之后把這個(gè)等待項(xiàng)給掛載到 eventpoll 的 “等待隊(duì)列” 也就是那個(gè) wq 上。
- 將當(dāng)前線程從操作系統(tǒng)的調(diào)度隊(duì)列中拿出來(lái),hang 住當(dāng)前線程。(current 總是指向當(dāng)前正在運(yùn)行的線程,內(nèi)部是通過(guò)匯編+寄存器實(shí)現(xiàn)的,這里可以當(dāng)成一個(gè)全局的環(huán)境變量)
所以簡(jiǎn)單來(lái)說(shuō) epoll_wait 做的最主要的事兒就是往內(nèi)部的 “等待隊(duì)列” 中插入了一個(gè) “等待項(xiàng)” 并且讓當(dāng)前線程睡覺。接下來(lái)我們來(lái)看上邊比較重要的一個(gè) “等待項(xiàng)” 是啥
簡(jiǎn)單理解,所謂等待項(xiàng)就是一個(gè)結(jié)構(gòu)體,上邊會(huì)放一個(gè) private 屬性,該屬性指向 current 也就是當(dāng)前線程的 task_struct 結(jié)構(gòu)體,還有個(gè) func 屬性指向一個(gè)名叫 “default_wake_function” 的回調(diào)函數(shù)。
然后這個(gè)等待項(xiàng),就會(huì)被插入到 eventpoll 的 wq “等待隊(duì)列” 上
到這兒為止,我們就把 epoll_wait 主要做的事情也說(shuō)完了。
4、幾個(gè)系統(tǒng)調(diào)用總結(jié)
接下來(lái)我們簡(jiǎn)單總結(jié)一下,epoll_create 和 epoll_ctl 以及 epoll_wait 都大概做了哪些事情:
首先是 epoll_create,它是使用 epoll 的第一步,它里邊主要是創(chuàng)建了三個(gè)數(shù)據(jù)結(jié)構(gòu),一個(gè) “等待隊(duì)列”,一個(gè) “就緒隊(duì)列”,以及一顆 “紅黑樹”。
如果用偽代碼表示的話,那么當(dāng)你調(diào)用了 epoll_create 之后,此時(shí)通過(guò)這個(gè)系統(tǒng)調(diào)用返回的 fd,你能拿到這么一個(gè)結(jié)構(gòu)體:
等待隊(duì)列 = [],
就緒隊(duì)列 = [],
紅黑樹 = [],
}
然后是 epoll_ctl,它允許你將實(shí)現(xiàn)了 poll 方法的文件系統(tǒng)作為參數(shù)交給 epoll 管理,epoll_ctl 內(nèi)部會(huì)調(diào)用真實(shí)的底層協(xié)議實(shí)現(xiàn)的 poll 方法,并把 epoll 這一層的一個(gè)回調(diào)函數(shù)作為參數(shù)傳給 poll 方法,然后底層協(xié)議的 poll 方法中會(huì)調(diào)用 epoll 傳進(jìn)來(lái)的那個(gè)回調(diào)函數(shù),并且協(xié)議會(huì)把自己身上的等待隊(duì)列作為參數(shù)交給 epoll 的那個(gè)回調(diào)函數(shù)來(lái)處理。而這個(gè)回調(diào)函數(shù)中則會(huì)創(chuàng)建一個(gè)等待項(xiàng),這個(gè)等待項(xiàng)上有個(gè)回調(diào)函數(shù)叫 “ep_poll_callback”,并且把這個(gè)等待項(xiàng)給塞到底層協(xié)議傳過(guò)來(lái)的等待隊(duì)列上。
如果用偽代碼表示的話,那么當(dāng)你調(diào)用了 epoll_ctl 并把一個(gè) socket 交給它管理之后,此時(shí) fd 對(duì)應(yīng)的結(jié)構(gòu)體就變成了這樣,它的紅黑樹中會(huì)多一個(gè)節(jié)點(diǎn):
等待隊(duì)列 = [],
就緒隊(duì)列 = [],
紅黑樹 = [
socket1 = {
等待隊(duì)列 = [{ callback: ep_poll_callback }]
}
],
}
最后是 epoll_wait,它會(huì) hang 住當(dāng)前線程,以等待被托管的 fd 身上有 IO 事件發(fā)生。它內(nèi)部會(huì)創(chuàng)建一個(gè)等待項(xiàng),注意這個(gè)等待項(xiàng)和上邊 epoll_ctl 中的那個(gè)等待項(xiàng)不是一個(gè)東西,上邊 epoll_ctl 的等待項(xiàng)是塞給了 socket 的等待隊(duì)列,而且里頭只有一個(gè)叫 “ep_poll_callback” 的回調(diào)函數(shù),而這里的 epoll_wait 的等待項(xiàng)是真的塞給了 epoll 自己的 eventpoll 上的等待隊(duì)列,并且它上邊除了有個(gè)一個(gè)叫做 “default_wake_function” 的回調(diào)函數(shù),同時(shí)還保存了 current 也就是當(dāng)前線程對(duì)應(yīng)的 task_struct 結(jié)構(gòu)體。都弄完了之后就會(huì)出讓 cpu 讓當(dāng)前線程睡覺覺。
如果用偽代碼表示的話,那么當(dāng)你調(diào)用了 epoll_wait 之后,此時(shí)的 fd 能找到的結(jié)構(gòu)體就變成了這樣,這個(gè) epoll 自己的等待隊(duì)列上會(huì)多一個(gè)等待項(xiàng):
等待隊(duì)列 = [{ callback: default_wake_function, private: current }],
就緒隊(duì)列 = [],
紅黑樹 = [
socket1 = {
等待隊(duì)列 = [{ callback: ep_poll_callback }]
}
],
}
5、 當(dāng)來(lái)消息了
當(dāng)用戶態(tài)執(zhí)行完了上邊仨系統(tǒng)調(diào)用之后,這條線程就 hang 在這兒了,知道有客戶端發(fā)消息過(guò)來(lái)。那么接下來(lái)我們看看當(dāng)用戶發(fā)消息過(guò)來(lái)之后會(huì)發(fā)生什么。
太具體的網(wǎng)卡收包的過(guò)程咱們就不說(shuō)了先,大概過(guò)程總之就是網(wǎng)卡收到數(shù)據(jù)之后觸發(fā)硬中斷以及軟中斷,軟中斷從緩沖區(qū)中把收到的數(shù)據(jù)處理成 sk_buffer 這個(gè)數(shù)據(jù)結(jié)構(gòu),然后從網(wǎng)卡驅(qū)動(dòng)也就是鏈路層這一層開始往上送到網(wǎng)絡(luò)層再送到傳輸層,在網(wǎng)絡(luò)層將 sk_buffer 送到傳輸層之前,它要有一步是根據(jù) sk_buffer 中的協(xié)議,來(lái)找到要使用哪個(gè)傳輸層協(xié)議:
在 tcp 對(duì)應(yīng)的 tcp_v4_rc 方法中,就會(huì)根據(jù) ip 以及 port 去查找對(duì)應(yīng)的 socket:
struct sock *sk;
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
ret = tcp_v4_do_rcv(sk, skb);
}
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb) {
tcp_rcv_established(sk, skb);
}
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb) {
tcp_data_ready(sk);
}
void tcp_data_ready(struct sock *sk) {
sk->sk_data_ready(sk);
}
我們順著 tcp_v4_rcv 這條調(diào)用鏈看下去,最后會(huì)發(fā)現(xiàn)最終會(huì)調(diào)用到一個(gè)叫 “sk->sk_data_ready” 的方法,這個(gè)方法從名字上看就能看出來(lái),它的作用是當(dāng) “數(shù)據(jù)準(zhǔn)備好了” 時(shí)候調(diào)用的,那么這個(gè) sk_data_ready 是誰(shuí)呢?其實(shí)這個(gè)方法是在當(dāng)用戶創(chuàng)建 socket 以及內(nèi)部的 sock 結(jié)構(gòu)時(shí)候被掛到上邊去的,由于創(chuàng)建 socket 的過(guò)程也比較繁瑣,這里我們就不再細(xì)說(shuō)了,在之前的文章中我們有過(guò)介紹。我們記住一個(gè)結(jié)論,就是這個(gè) sk_data_ready 屬性,指向的就是一個(gè)叫做 “sock_def_readable” 的方法:
{
struct socket_wq *wq;
wq = rcu_dereference(sk->sk_wq);
wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN | EPOLLPRI |
EPOLLRDNORM | EPOLLRDBAND);
}
該方法中通過(guò) “wake_up_interruptible_sync_poll” 方法執(zhí)行等待隊(duì)列上的回調(diào)函數(shù)。不過(guò)這里有個(gè)迷惑人的地方,就是它的名字雖然帶有 “wake_up” 喚醒字樣,但實(shí)際上這里其實(shí)并一定會(huì)喚醒當(dāng)前線程。如果你在上層是對(duì) socket 做了類似 recv 之類的操作的話,那確實(shí)這里是會(huì)做喚醒,但是在 epoll 的情況下,這里并不會(huì)直接喚醒線程,為啥呢?還記得上邊我們介紹 epoll 的三個(gè)相關(guān)系統(tǒng)調(diào)用,當(dāng)你把 epoll_create、epoll_ctl、epoll_wait 仨東西都調(diào)用完之后,通過(guò) fd 能拿到的是啥玩意兒么,來(lái)看下邊回顧下:
等待隊(duì)列 = [{ callback: default_wake_function, private: current }],
就緒隊(duì)列 = [],
紅黑樹 = [
socket1 = {
等待隊(duì)列 = [{ callback: ep_poll_callback }]
}
],
}
就是這么個(gè)玩意兒,它里頭有兩個(gè) “等待隊(duì)列”,其中一個(gè)在 socket 上,另外一個(gè)在 epoll 上。這里這個(gè) “sock_def_readable” 方法中的 “wake_up_interruptible_sync_poll” 其實(shí)是會(huì)去 socket 上的等待隊(duì)列中去拿那個(gè)等待項(xiàng),這個(gè)等待項(xiàng)里只有一個(gè) callback 指向了 ep_poll_callback 回調(diào)函數(shù)。其實(shí)對(duì)于非 epoll 的情況下,如果上層調(diào)用的 recv 的話, 這個(gè) socket 的等待項(xiàng)中,確實(shí)是會(huì)還有個(gè) private 指向 current 的,不過(guò)這里我們是 epoll 的場(chǎng)景,對(duì)于其他場(chǎng)景大家可以自行研究,如果把 epoll 這個(gè)場(chǎng)景整明白了,其他場(chǎng)景其實(shí)也大同小異。
總之呢,這里會(huì)調(diào)用這個(gè) socket 的等待隊(duì)列中的 ep_poll_callback 方法:
// 先找到這個(gè) socket 對(duì)應(yīng)的紅黑樹上的那個(gè)節(jié)點(diǎn)
struct epitem *epi = ep_item_from_wait(wait);
// 再找到管理著這個(gè) socket 的那個(gè) eventpoll 結(jié)構(gòu)體
struct eventpoll *ep = epi->ep;
// 把這個(gè) socket 對(duì)應(yīng)的紅黑樹的那個(gè)節(jié)點(diǎn)給添加到 eventpoll 的就緒隊(duì)列中
list_add_tail_lockless(&epi->rdllink, &ep->rdllist)
// 看 eventpoll 的等待隊(duì)列中是否有等待項(xiàng), 然后嘗試喚醒
if (waitqueue_active(&ep->wq)) {
wake_up(&ep->wq);
}
}
也就是說(shuō),當(dāng)托管給 epoll 的某個(gè) socket 上接收到了消息之后,tcp 的協(xié)議棧那層會(huì)主動(dòng)觸發(fā)一個(gè)喚醒用的 callback,這個(gè) callback 是 “ep_poll_callback”,然后這個(gè) “ep_poll_callback” 中又會(huì)找到紅黑樹上對(duì)應(yīng)的節(jié)點(diǎn),并把這個(gè)節(jié)點(diǎn)放到 epoll 內(nèi)部的 “就緒隊(duì)列中”,此時(shí)的偽代碼可表示為:
等待隊(duì)列 = [{ callback: default_wake_function, private: current }],
就緒隊(duì)列 = [ socket1 ],
紅黑樹 = [],
}
簡(jiǎn)單來(lái)講就是當(dāng)某個(gè) socket 收到消息后,這個(gè) socket 就不在紅黑樹里呆著了,會(huì)被放到 epoll 的就緒隊(duì)列中。之后觸發(fā) “wake_up” 方法,該方法就會(huì)去 epoll 自己的等待隊(duì)列上去看是否有等待項(xiàng),有的話觸發(fā)它的 callback,這里如上偽代碼表示,就是觸發(fā)了 “default_wake_function” 方法:
里邊觸發(fā)了一個(gè) try_to_wake_up,我們注意看這個(gè)函數(shù)的參數(shù)是誰(shuí),是一個(gè)叫 “curr->private” 的東西,這個(gè)是誰(shuí)呢?誒!就是上邊偽代碼中 epoll 的等待隊(duì)列中的等待項(xiàng)里的 private 對(duì)應(yīng)的那個(gè) current,也就是之前調(diào)用了 epoll_wait 的那條線程對(duì)應(yīng)的 task_struct。
換句話說(shuō),當(dāng)調(diào)用了 try_to_wake_up(curr -> private) 之后,這條被 hang 住的線程,就會(huì)被重新加入到可運(yùn)行的任務(wù)隊(duì)列中,操作系統(tǒng)會(huì)在適當(dāng)?shù)臅r(shí)機(jī)繼續(xù)執(zhí)行它。
那么重新回到哪兒執(zhí)行呢?還記得我們是在哪里 hang 住當(dāng)前線程的么?是在調(diào)用了 epoll_wait 時(shí),內(nèi)部執(zhí)行了一個(gè)叫做 “ep_poll” 的方法里邊 hang 住的,忘了的話可以往上翻一番看一看那個(gè) “ep_poll” 方法。所以繼續(xù)執(zhí)行的話,就可以執(zhí)行到 “ep_send_events”,也就是會(huì)把當(dāng)前就緒隊(duì)列中的東西返回給用戶態(tài),最后就是用戶態(tài)拿到咔咔用就行了~
到這兒,我們總結(jié)一下當(dāng)數(shù)據(jù)包來(lái)了之后會(huì)發(fā)生了:
- 網(wǎng)卡收到包后一路往上送,送到 tcp 那層后
- tcp 那層會(huì)根據(jù) ip 和 port 找到對(duì)應(yīng)的 socket
- 觸發(fā) socket 上的喚醒函數(shù)
- 該函數(shù)主要是從 socket 的等待隊(duì)列中獲取等待項(xiàng),并觸發(fā)其中的回調(diào)函數(shù)
- 這個(gè)回調(diào)函數(shù)中會(huì)找到這個(gè) socket 對(duì)應(yīng)的紅黑樹節(jié)點(diǎn),并把這個(gè)節(jié)點(diǎn)加入到 epoll 自己的 “就緒隊(duì)列” 中
- 最后查看 epoll 自己的 “等待隊(duì)列” 中,是否有等待項(xiàng),有的話觸發(fā)其中的回調(diào)函數(shù)
- 這個(gè)回調(diào)函數(shù)會(huì)拿到之前保存的 private 屬性,也就是 task_struct 進(jìn)行線程喚醒
- 喚醒后的線程從之前 hang 住的地方重新開始執(zhí)行,會(huì)把 epoll “就緒隊(duì)列” 中的都吐給用戶態(tài)去使用
epoll 的性能高在哪兒?
到這里我們終于說(shuō)完了 epoll 的基本實(shí)現(xiàn)原理,現(xiàn)在我們可以回過(guò)頭來(lái)看一看,都說(shuō) epoll 性能高,那到底高在哪兒呢?
我們首先來(lái)看當(dāng)不使用 epoll 的時(shí)候,我們可能會(huì)這么用 socket:
for {
conn = accpet(listenfd)
// 開個(gè)新線程或者從線程池里撈一條線程去處理 conn
// 這條線程里去 read,write
start_new_process(conn)
}
我們會(huì)先死循環(huán)中等待客戶端的鏈接,每來(lái)一個(gè)鏈接,就開啟一條新線程或者從池子里撈,用這條線程去處理 conn。
當(dāng)我們使用 epoll 的時(shí)候,我們可能會(huì)這么用:
epoll_ctl(listenfd)
for {
nums = epoll_wait(&events)
for (i = 0; i < nums; i++) {
if (events[i].data.fd == listenfd) {
connfd = accpet(listenfd)
epoll_ctl(connfd)
} else {
connfd = ep[i].data.fd
// 開個(gè)新線程或者從線程池里撈一條線程去處理 conn
// 這條線程里去 read, write
start_new_process(connfd)
}
}
}
可以看到里頭其實(shí)也是會(huì)頻繁的創(chuàng)建新線程或者從池子里撈一條線程出來(lái)用。乍一看之下,感覺用不用 epoll 好像沒(méi)啥差別。但是實(shí)際上,我們可以細(xì)想一下,如果用第一種方式,我們將 accept 的 fd 交給一條新的線程之后,在其內(nèi)部我們一般會(huì)怎么做呢?一般可能就是:
while(true) {
res = recv(acceptfd);
}
}
我們?cè)谛碌木€程中處理每個(gè)鏈接時(shí),大概率還是會(huì)用個(gè)死循環(huán)然后里頭不停地去 hang 住線程知道有用戶發(fā)請(qǐng)求過(guò)來(lái)。那么此時(shí)這條線程就卡死在這兒了。那么如果這條線程是從線程池中撈出來(lái)的話,這條線程就暫時(shí)回不去池子里了,相當(dāng)于我們可用的線程資源就少一個(gè)。
但是對(duì)于 epoll 的場(chǎng)景來(lái)講,epoll 是一定能保證當(dāng)前用戶拿到的這個(gè) fd 中,確定一定以及肯定是有事件發(fā)生了,所以我們即使會(huì)創(chuàng)建新的線程或者從池子里撈,也可以馬上就讓這條新的線程去對(duì)我們拿到的 fd 做處理,就不用再 hang 住這條線程了。也就是說(shuō)我們可以高效地利用每一條線程。這就是 epoll 高性能的原因。
如果用 epoll 托管 epoll 會(huì)怎么樣?
回到我們的標(biāo)題,我們?cè)谏线叺奈恼轮姓f(shuō)過(guò),當(dāng)你的文件系統(tǒng)實(shí)現(xiàn)了 poll 方法之后,就可以使用 epoll 來(lái)托管,我們也說(shuō)過(guò) epoll 自己就是一種文件系統(tǒng),那么我們來(lái)看看 epoll 這個(gè)文件系統(tǒng)它能做哪些操作:
能看到它里頭其實(shí)也實(shí)現(xiàn)了 poll 方法,所以理論上來(lái)說(shuō)我們就可以用 epoll 去托管 epoll。對(duì)于這個(gè) “ep_eventpoll_poll” 方法,里面主要調(diào)用了一個(gè) “poll_wait” 方法:
而對(duì)于 “poll_wait” 方法,它主要是調(diào)用了一個(gè) “_qproc” 方法。怎么樣這個(gè)方法是不是眼熟,這個(gè)就和我們?cè)谏线吔榻B用 epoll 管理 socket 時(shí)一樣,epoll_ctl 會(huì)調(diào)用 socket 的 poll 方法,然后這個(gè) poll 方法中又會(huì)調(diào)用上層 epoll 傳過(guò)來(lái)的那個(gè)回調(diào)函數(shù)。
后邊的事情大家就可以嘗試自己去分析分析了,這里因?yàn)檫^(guò)程和 socket 是差不多的,我就不再一點(diǎn)點(diǎn)分析了,我們可以直接用偽代碼來(lái)表示,如果用 epoll 托管 epoll,最后的數(shù)據(jù)結(jié)構(gòu)體的樣子,大概如下:
等待隊(duì)列 = [{ private: current, callback: default_wake_function }],
就緒隊(duì)列 = [],
紅黑樹 = [
socket2 = {
等待隊(duì)列 = [{ private: null, callback: ep_poll_callback }]
},
ep1 = {
等待隊(duì)列 = [{ callback: ep_poll_callback }],
就緒隊(duì)列 = [],
紅黑樹 = [
socket1 = {
等待隊(duì)列 = [{ private: null, callback: ep_poll_callback }]
}
],
}
],
}
簡(jiǎn)單來(lái)講,就是內(nèi)部的 epoll 的等待隊(duì)列中的等待項(xiàng),其實(shí)回調(diào)函數(shù)和 socket 的等待項(xiàng)中一樣,也是 “ep_poll_callback” 方法,只有外層的 epoll 的等待項(xiàng)中才會(huì)保存當(dāng)前線程的 current。
也就是說(shuō)!如果我們用 epoll 去管理一個(gè) epoll 會(huì)發(fā)生什么呢!
答案是其實(shí)啥也不會(huì)發(fā)生,和正常一樣,當(dāng)外層的 epoll 有了就緒事件之后,用戶側(cè)拿到的 fd 除了是 socket 的 fd,還有可能是個(gè)內(nèi)部 epoll 的 fd,這個(gè) epoll 如果想從它上邊獲取到內(nèi)部 socket 的消息,我們還是需要對(duì)內(nèi)部的這個(gè) epoll 做正常的 epoll_wait 等操作。我這里有個(gè)簡(jiǎn)單的小 demo,大家感興趣的話可以自己嘗試一下玩一玩:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT1 13190
#define PORT2 13191
#define MAX 1023
int set_fcntl(int rws)
{
int flags = fcntl(rws, F_GETFD);
if (flags < 0)
{
perror("get fcntl errnor");
return -1;
}
flags |= O_NONBLOCK;
if (fcntl(rws, F_SETFD, flags) < 0)
{
perror("set fcntl errnor");
return -1;
}
return 0;
}
int main() {
pid_t pid = getppid();
printf("本條進(jìn)程的 pid 是: %dn", pid);
// 創(chuàng)建 socket1
int sockfd1, sockfd2;
struct sockaddr_in myaddr1, myaddr2;
sockfd1 = socket(AF_INET, SOCK_STREAM, 0);
sockfd2 = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd1 < 0 || sockfd2 < 0) {
perror("creat sockfd1 failed");
return -1;
}
int on = 1;
if (
setsockopt(sockfd1, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0 ||
setsockopt(sockfd2, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0
) {
perror("setsockopt");
return -1;
}
myaddr1.sin_family = AF_INET;
myaddr1.sin_port = htons(PORT1);
myaddr1.sin_addr.s_addr = INADDR_ANY;
myaddr2.sin_family = AF_INET;
myaddr2.sin_port = htons(PORT2);
myaddr2.sin_addr.s_addr = INADDR_ANY;
if (
bind(sockfd1, (const struct sockaddr *)&myaddr1, sizeof(myaddr1)) < 0 ||
bind(sockfd2, (const struct sockaddr *)&myaddr2, sizeof(myaddr2)) < 0
) {
perror("bind failed");
return -1;
}
if (
listen(sockfd1, 10) < 0 ||
listen(sockfd2, 10) < 0
) {
perror("listen failed");
return -1;
}
int efd = epoll_create(2);
int efd_internal = epoll_create(1);
if (efd < 0 || efd_internal < 0) {
printf("efd errnon");
return -1;
}
int cn_fd1 = accept(sockfd1, NULL, NULL);
int cn_fd2 = accept(sockfd2, NULL, NULL);
set_fcntl(cn_fd1);
set_fcntl(cn_fd2);
if (cn_fd1 < 0 || cn_fd2 < 0) {
printf("accept fd errnorn");
return -1;
}
struct epoll_event evt1 = {
.events = EPOLLIN,
.data.fd = cn_fd1,
};
struct epoll_event evt2 = {
.events = EPOLLIN,
.data.fd = cn_fd2,
};
struct epoll_event evt_internal = {
.events = EPOLLIN,
.data.fd = efd_internal,
};
// 把 fd1 添加到外部的 epoll 中
if (epoll_ctl(efd, EPOLL_CTL_ADD, cn_fd1, &evt1) < 0) {
printf("put listen_fd epoll errnon");
return -1;
}
// 把 fd2 添加到內(nèi)部的 epoll
if (epoll_ctl(efd_internal, EPOLL_CTL_ADD, cn_fd2, &evt2) < 0) {
printf("put listen_fd epoll errnon");
return -1;
}
// 把內(nèi)部的 epoll 添加到外部的 epoll 中
if (epoll_ctl(efd, EPOLL_CTL_ADD, efd_internal, &evt_internal) < 0) {
printf("put listen_fd epoll errnon");
return -1;
}
char buf[1024] = {0};
struct epoll_event events[MAX];
while (1) {
int i = 0;
// 這里再 wait 時(shí), 要么是 fd1 收到數(shù)據(jù), 要么是內(nèi)部的 epoll 的 fd2 收到數(shù)據(jù)
int num = epoll_wait(efd, events, MAX, ~0);
if (num < 0) {
printf("epoll_wait events start errnon");
return -1;
}
for (i = 0; i < num; i++) {
if (events[i].events & EPOLLIN) {
if (events[i].data.fd == cn_fd1) {
printf("外部的 fd1 接收到數(shù)據(jù)n");
int len = read(cn_fd1, buf, sizeof(buf));
if (len <= 0) {
struct epoll_event ac_evt1;
if (epoll_ctl(efd, EPOLL_CTL_DEL, cn_fd1, &ac_evt1) < 0) {
printf("put accept_fd epoll errnon");
return -1;
}
close(cn_fd1);
} else {
printf("%sn", buf);
write(events[i].data.fd, buf, len);
}
} else if (events[i].data.fd == cn_fd2) {
printf("外部的 fd2 接收到數(shù)據(jù)n");
int len = read(cn_fd1, buf, sizeof(buf));
if (len <= 0) {
struct epoll_event ac_evt1;
if (epoll_ctl(efd, EPOLL_CTL_DEL, cn_fd1, &ac_evt1) < 0) {
printf("put accept_fd epoll errnon");
return -1;
}
close(cn_fd1);
} else {
printf("%sn", buf);
write(events[i].data.fd, buf, len);
}
} else if (events[i].data.fd == efd_internal) {
printf("內(nèi)部的 epoll 接收到數(shù)據(jù)n");
char buf_internal[1024] = {0};
struct epoll_event events_internal[MAX];
int num_internal = epoll_wait(efd_internal, events_internal, MAX, ~0);
if (num_internal < 0) {
printf("internal epoll_wait events start errnon");
return -1;
}
int i_internal = 0;
for (i_internal = 0; i_internal < num_internal; i_internal++) {
if (events_internal[i].events & EPOLLIN) {
if (events_internal[i].data.fd == cn_fd2) {
printf("內(nèi)部的 fd2 接收到數(shù)據(jù)n");
int len = read(cn_fd2, buf_internal, sizeof(buf_internal));
if (len <= 0) {
struct epoll_event ac_evt2;
if (epoll_ctl(efd_internal, EPOLL_CTL_DEL, cn_fd2, &ac_evt2) < 0) {
printf("put internal accept_fd epoll errnon");
return -1;
}
close(cn_fd2);
} else {
printf("%sn", buf_internal);
write(cn_fd2, buf_internal, len);
}
}
}
}
}
}
}
}
}
到這里,我們就把 epoll 的實(shí)現(xiàn)原理,以及為啥性能好,還有一個(gè)不常見的小場(chǎng)景都介紹了一下。
-
Socket
+關(guān)注
關(guān)注
0文章
212瀏覽量
34731 -
源碼
+關(guān)注
關(guān)注
8文章
643瀏覽量
29264 -
代碼
+關(guān)注
關(guān)注
30文章
4797瀏覽量
68707 -
epoll
+關(guān)注
關(guān)注
0文章
28瀏覽量
2967
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論