系列文章:
- 深入理解Linux網(wǎng)絡(luò)——內(nèi)核是如何接收到網(wǎng)絡(luò)包的
- 深入理解Linux網(wǎng)絡(luò)——內(nèi)核與用戶進(jìn)程協(xié)作之同步阻塞方案(BIO)
- 深入理解Linux網(wǎng)絡(luò)——內(nèi)核與用戶進(jìn)程協(xié)作之多路復(fù)用方案(epoll)
- 深入理解Linux網(wǎng)絡(luò)——內(nèi)核是如何發(fā)送網(wǎng)絡(luò)包的
- 深入理解Linux網(wǎng)絡(luò)——本機(jī)網(wǎng)絡(luò)IO
- 深入理解Linux網(wǎng)絡(luò)——TCP連接建立過(guò)程(三次握手源碼詳解)
- 深入理解Linux網(wǎng)絡(luò)——TCP連接的開(kāi)銷
在上一部分中講述了網(wǎng)絡(luò)包是如何從網(wǎng)卡送到協(xié)議棧的(詳見(jiàn)深入理解Linux網(wǎng)絡(luò)——內(nèi)核是如何接收到網(wǎng)絡(luò)包的),接下來(lái)內(nèi)核還有一項(xiàng)重要的工作,就是在協(xié)議棧接收處理完輸入包后要通知到用戶進(jìn)程,如何用戶進(jìn)程接收到并處理這些數(shù)據(jù)。
進(jìn)程與內(nèi)核配合有多種方案,這里我們這分析兩種典型的:
-
同步阻塞方案(Java中習(xí)慣叫BIO)
-
多路IO復(fù)用方案(Java中對(duì)應(yīng)NIO)
- Linux多路復(fù)用有select、poll、epoll,這里只講性能最優(yōu)秀的epoll
本文主要講的是同步阻塞模式的實(shí)現(xiàn)方案,多路IO復(fù)用方案及問(wèn)題解答見(jiàn)文章深入理解Linux內(nèi)核網(wǎng)絡(luò)——內(nèi)核與用戶進(jìn)程協(xié)作之多路復(fù)用方案(epoll)
一、相關(guān)實(shí)際問(wèn)題
- 阻塞到底是怎么一回事
- 同步阻塞IO都需要哪些開(kāi)銷
- 多路復(fù)用epoll為什么就能提高網(wǎng)絡(luò)性能
- epoll也是阻塞的嗎
- redis為什么網(wǎng)絡(luò)性能突出
二、socket的直接創(chuàng)建
以開(kāi)發(fā)者的角度來(lái)看,調(diào)用socket函數(shù)可以創(chuàng)建一個(gè)socket
int main()
{
int sk = socket(AF_INET, SOCK_STREAM, 0);
......
}
等這個(gè)socket函數(shù)調(diào)用執(zhí)行完以后,用戶層面看到返回的是一個(gè)整數(shù)型的句柄,但其實(shí)內(nèi)核在內(nèi)部創(chuàng)建了一系列的socket相關(guān)的內(nèi)核對(duì)象(不止一個(gè))。它們之間相互的關(guān)系如下:
socket在內(nèi)核中的定義如下:
struct socket {
socket_state state;
unsigned long flags;
const struct proto_ops *ops;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
};
typedef enum {
SS_FREE = 0, //該socket還未分配
SS_UNCONNECTED, //未連向任何socket
SS_CONNECTING, //正在連接過(guò)程中
SS_CONNECTED, //已連向一個(gè)socket
SS_DISCONNECTING //正在斷開(kāi)連接的過(guò)程中
}socket_state;
socket是內(nèi)核抽象出的一個(gè)通用結(jié)構(gòu)體,主要是設(shè)置了一些跟fs相關(guān)的字段,而真正跟網(wǎng)絡(luò)通信相關(guān)的字段結(jié)構(gòu)體是struct sock。
struct sock是網(wǎng)絡(luò)層對(duì)于struct socket的表示,其中成員非常多,這里只介紹其中一部分。
sk_prot和sk_prot_creator,這兩個(gè)成員指向特定的協(xié)議處理函數(shù)集,其類型是結(jié)構(gòu)體struct proto,該結(jié)構(gòu)體也是跟struct proto_ops相似的一組協(xié)議操作函數(shù)集。這兩者之間的概念似乎有些混淆,可以這么理解,struct proto_ops的成員操作struct socket層次上的數(shù)據(jù),處理完了,再由它們調(diào)用成員sk->sk_prot的函數(shù),操作struct sock層次上的數(shù)據(jù)。即它們之間存在著層次上的差異。struct proto類型的變量在協(xié)議棧中總共也有三個(gè),分別是mytcp_prot,myudp_prot,myraw_prot,對(duì)應(yīng)TCP, UDP和RAW協(xié)議。
sk_state表示socket當(dāng)前的連接狀態(tài),是一個(gè)比struct socket的state更為精細(xì)的狀態(tài),其可能的取值如下:
enum { TCP_ESTABLISHED = 1, TCP_SYN_SENT, TCP_SYN_RECV, TCP_FIN_WAIT1, TCP_FIN_WAIT2, TCP_TIME_WAIT, TCP_CLOSE, TCP_CLOSE_WAIT, TCP_LAST_ACK, TCP_LISTEN, TCP_CLOSING, TCP_MAX_STATES; }
- 這些取值從名字上看,似乎只使用于TCP協(xié)議,但事實(shí)上,UDP和RAW也借用了其中一些值,在一個(gè)socket創(chuàng)建之初,其取值都是TCP_CLOSE,一個(gè)UDP socket connect完成后,將這個(gè)值改為T(mén)CP_ESTABLISHED,最后,關(guān)閉sockt前置回TCP_CLOSE,RAW也一樣。
sk_rcvbuf和sk_sndbuf:表示接收和發(fā)送緩沖區(qū)的大小。這兩個(gè)值是動(dòng)態(tài)的,應(yīng)用程序可以通過(guò)setsockopt系統(tǒng)調(diào)用來(lái)改變它們的值。但是,這些值也受到了一些全局內(nèi)核參數(shù)的限制(通常由/proc/sys/net/core/rmem_default(對(duì)于接收緩沖區(qū))和/proc/sys/net/core/wmem_default(對(duì)于發(fā)送緩沖區(qū))這兩個(gè)內(nèi)核參數(shù)來(lái)決定)。
sk_receive_queue和sk_write_queue:接收緩沖隊(duì)列和發(fā)送緩沖隊(duì)列,隊(duì)列里排列的是套接字緩沖區(qū)struct sk_buff,隊(duì)列中的struct sk_buff的字節(jié)數(shù)總和不能超過(guò)緩沖區(qū)大小的設(shè)定。在sock實(shí)例創(chuàng)建的時(shí)候初始化的,最開(kāi)始為空的隊(duì)列(雙向鏈表)。
struct inet_sock:這是INET域?qū)S玫囊粋€(gè)socket表示,它是在struct sock的基礎(chǔ)上進(jìn)行的擴(kuò)展,在基本socket的屬性已具備的基礎(chǔ)上,struct inet_sock提供了INET域?qū)S械囊恍傩?,比如TTL,組播列表,IP地址,端口等,完整定義如下:
struct inet_sock { struct sock sk; #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE) struct ipv6_pinfo *pinet6; #endif __u32 daddr; //IPv4的目的地址。 __u32 rcv_saddr; //IPv4的本地接收地址。 __u16 dport; //目的端口。 __u16 num; //本地端口(主機(jī)字節(jié)序)。 __u32 saddr; //發(fā)送地址。 __s16 uc_ttl; //單播的ttl。 __u16 cmsg_flags; struct ip_options *opt; __u16 sport; //源端口。 __u16 id; //單調(diào)遞增的一個(gè)值,用于賦給iphdr的id域。 __u8 tos; //服務(wù)類型。 __u8 mc_ttl; //組播的ttl __u8 pmtudisc; __u8 recverr:1, is_icsk:1, freebind:1, hdrincl:1, //是否自己構(gòu)建ip首部(用于raw協(xié)議) mc_loop:1; //組播是否發(fā)向回路。 int mc_index; //組播使用的本地設(shè)備接口的索引。 __u32 mc_addr; //組播源地址。 struct ip_mc_socklist *mc_list; //組播組列表。 struct { unsigned int flags; unsigned int fragsize; struct ip_options *opt; struct rtable *rt; int length; u32 addr; struct flowi fl; } cork; };
sock_create是創(chuàng)建socket的主要位置,其中sock_create又調(diào)用了__sock_create
int __sock_create(struct net *net, int family, ...)
{
struct socket *sock;
const struct net_proto_family *pf;
......
// 分配socket對(duì)象
sock = sock_alloc();
// 獲得每個(gè)協(xié)議族的操作表
pf = rcu_dereference(net_families[family]
// 調(diào)用指定協(xié)議族的創(chuàng)建函數(shù),對(duì)于AF_INET對(duì)應(yīng)的就是inet_creat
err = pf->create(net, sock, protocol, kern);
}
在__sock_create里,首先調(diào)用sock_alloc來(lái)分配一個(gè)struct socket的內(nèi)核對(duì)象,接著獲取協(xié)議族的操作函數(shù)表并調(diào)用其create方法,對(duì)于AF_INET協(xié)議族來(lái)說(shuō),執(zhí)行的是inet_create方法。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct sock *sk;
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
// 將inet_stream_ops賦值到socket->ops上
sock->ops = answer->ops;
// 獲得tcp_prot
answer_prot = answer->prot;
// 分配sock對(duì)象,并把tcp_prot賦值到sk->prot上
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
// 對(duì)sock對(duì)象進(jìn)行初始化
sock_init_data(sock, sk);
}
}
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM;
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.no_check = 0,
.flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
},
}
在inet_create中,根據(jù)類型SOCK_STREAM查找到對(duì)于TCP定義的操作方法實(shí)現(xiàn)集合inet_stream_ops和tcp_prot,并把它們發(fā)別設(shè)置到socket->ops和sk->prot上。
最后的sock_init_data將sk中的sk_data_ready函數(shù)指針進(jìn)行了初始化(也包括設(shè)置其他函數(shù)指針),設(shè)置為默認(rèn)的sock_def_readable,同時(shí)也會(huì)初始化sk_receive_queue和sk_write_queue為空隊(duì)列
inetsw_array存儲(chǔ)了AF_INET類型套接字的的所有網(wǎng)絡(luò)協(xié)議
當(dāng)軟中斷上收到數(shù)據(jù)包時(shí)會(huì)通過(guò)調(diào)用sk_data_ready函數(shù)指針(實(shí)際上被設(shè)置成了sock_def_readable)來(lái)喚醒sock上等待的進(jìn)程。
至此一個(gè)tcp對(duì)象,確切的說(shuō)是AF_INET協(xié)議族下的SOCK_STREAM對(duì)象就算創(chuàng)建完成了,這里花費(fèi)了一次socket系統(tǒng)調(diào)用的開(kāi)銷。
三、內(nèi)核和用戶進(jìn)程協(xié)作之阻塞方式
同步阻塞IO總體流程如下
1)等待接收消息
查看recv函數(shù)的底層實(shí)現(xiàn)。首先通過(guò)strace命令追蹤,可以看到clib庫(kù)函數(shù)recv會(huì)執(zhí)行recvfrom系統(tǒng)調(diào)用。
進(jìn)入系統(tǒng)調(diào)用后,用戶進(jìn)程就進(jìn)入了內(nèi)核態(tài),執(zhí)行一系列的內(nèi)核協(xié)議層函數(shù),然后到socket對(duì)象的接收隊(duì)列中查看是否有數(shù)據(jù),沒(méi)有的話就把自己添加到socket對(duì)應(yīng)的等待隊(duì)列里然后讓出CPU,操作系統(tǒng)選擇下一個(gè)就緒狀態(tài)的進(jìn)程來(lái)執(zhí)行。
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
unsigned int, flags, struct sockaddr __user *, addr,
int __user *, addr_len)
{
struct socket *sock;
// 根據(jù)傳入的fd找到socket對(duì)象
sock = sock_lookup_light(fd, &err, &fput_needed);
......
err = sock_recvmsg(sock, &msg, size, flags);
......
}
后續(xù)的調(diào)用順序?yàn)椋?/p>
sock_recvmsg => __sock_recvmsg => __sock_recvmsg_nosec
在__sock_recvmsg_nosec中會(huì)去調(diào)用socket對(duì)象proto_ops里的recvmsg,在AF_INET中其指向的是inet_recvmsg方法。
而在inet_recvmg中,會(huì)去調(diào)用socket中的sock對(duì)象的sk->sk_prot->recvmsg,在SOCK_STREAM中它的實(shí)現(xiàn)是tcp_recvmsg方法。
int tcp_recvmsg(struct kiocb *iocb, strcut sock * sock, struct msghdr *msg,
size_t len, int nonblock, int flags, int *addr_len)
{
int copied = 0;
......
// 如果設(shè)置了MSG_WAITALL,則target==len,即recv函數(shù)中的參數(shù)len
// 如果沒(méi)設(shè)置MSG_WAITALL,則target==1
target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
do {
// 遍歷接收隊(duì)列接收數(shù)據(jù)
skb_queue_walk(&sk->sk_receive_queue, skb) {
......
}
......
}
if(copied >= target) {
release_sock(sk);
lock_sock(sk);
} else // 如果沒(méi)有收到足夠數(shù)據(jù),啟用sk_wait_data阻塞當(dāng)前進(jìn)程
sk_wait_data(sk, &timeo);
}
可以看到這里會(huì)去遍歷socket的接收隊(duì)列,如果接收到的數(shù)據(jù)不滿足目標(biāo)數(shù)量則會(huì)阻塞當(dāng)前進(jìn)程,具體阻塞方法的實(shí)現(xiàn)邏輯如下
int sk_wait_data(struct sock *sk, long *timeo)
{
// 當(dāng)前進(jìn)程(current)關(guān)聯(lián)到所定義的等待隊(duì)列項(xiàng)上
DEFINE_WAIT(wait);
// 調(diào)用sk_sleep獲取sock對(duì)象下的wait并準(zhǔn)備掛起,將進(jìn)程狀態(tài)設(shè)置為可打斷
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);
// 通過(guò)調(diào)用schedule_timeout讓出CPU,如何進(jìn)行睡眠
rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue);
......
}
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
#define DEFINE_WAIT_FUNC(name, function) wait_queue_t name = { \
.private = current \
.func = function \
.task_list = LIST_HEAD_INIT((name).task_list) }
首先在DEFINE_WAIT宏下**,定義了一個(gè)等待隊(duì)列項(xiàng)wait**,在這個(gè)新的等待隊(duì)列項(xiàng)上注冊(cè)了回調(diào)函數(shù)autoremove_wake_function,并把當(dāng)前進(jìn)程描述符current關(guān)聯(lián)到其.private成員上。
task_list = LIST_HEAD_INIT((name).task_list)將wait_queue_t的task_list成員初始化為一個(gè)空的鏈表頭。LIST_HEAD_INIT是一個(gè)宏,它接受一個(gè)list_head類型的變量,并將它初始化為一個(gè)空的鏈表頭。在這個(gè)宏定義中,(name).task_list實(shí)際上就是新定義的wait_queue_t變量的task_list成員。
所以,這行代碼的意思就是將新定義的wait_queue_t變量的task_list成員初始化為一個(gè)空的鏈表頭。這是必要的步驟,因?yàn)樵趙ait_queue_t被添加到等待隊(duì)列之前,它的task_list必須被初始化為一個(gè)有效的鏈表節(jié)點(diǎn)。prepare_to_wait()中會(huì)將wait變量的task_list成員添加到wait_queue_head_t類型的等待隊(duì)列中。也就是說(shuō),task_list成員會(huì)被鏈接到sk_sleep()返回的等待隊(duì)列中。
typedef struct __wait_queue_head wait_queue_head_t; struct __wait_queue_head { spinlock_t lock; struct list_head task_list; };
緊接著調(diào)用sk_sleep獲取sock對(duì)象下的等待隊(duì)列列表頭wait_queue_head_t。
接著調(diào)用prepare_to_wait來(lái)把新定義的等待隊(duì)列項(xiàng)wait插入sock對(duì)象的等待隊(duì)列,這樣后面當(dāng)內(nèi)核收完數(shù)據(jù)產(chǎn)生就緒事件的時(shí)候,就可以查找socket等待隊(duì)列上的等待項(xiàng),進(jìn)而可以找到回調(diào)函數(shù)和等待該socket就緒時(shí)間的進(jìn)程了。
最后調(diào)用sk_wait_event讓出CPU,進(jìn)程將進(jìn)入睡眠狀態(tài),這會(huì)導(dǎo)致一次進(jìn)程上下文的開(kāi)銷,而這個(gè)開(kāi)銷是昂貴的,大約需要花費(fèi)幾個(gè)微秒的CPU時(shí)間。
2)軟中斷模塊
上篇文章中我們講到了網(wǎng)絡(luò)包到網(wǎng)卡之后是怎么被網(wǎng)卡接收最后再交給軟中斷處理的,最后講到了ip_rcv根據(jù)inet_protos和數(shù)據(jù)包的協(xié)議將包交給上層協(xié)議棧的處理函數(shù)。軟中斷(也就是ksoftirqd線程)收到數(shù)據(jù)包以后,發(fā)現(xiàn)是TCP包就會(huì)執(zhí)行tcp_v4_rcv函數(shù),這里直接從TCP協(xié)議的接收函數(shù)tcp_v4_rcv開(kāi)始。
int tcp_v4_rcv(struct sk_buff *skb)
{
......
th = tcp_hdr(skb); // 獲取tcp header
iph = ip_hdr(skb); // 獲取ip header
// 根據(jù)數(shù)據(jù)包header中的IP、端口信息查找對(duì)應(yīng)的socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
......
// socket未被用戶鎖定
if(!sock_owned_by_user(sk)) {
{
if(!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
}
} else
// 如果數(shù)據(jù)包被用戶進(jìn)程鎖定,則數(shù)據(jù)包進(jìn)入后備處理隊(duì)列,并且該進(jìn)程進(jìn)入
// 套接字的后備處理等待隊(duì)列sk->lock.wq
sk_add_backlog(sk, skb);
}
首先根據(jù)收到的網(wǎng)絡(luò)包的header里的source和dest信息在本機(jī)上查詢對(duì)應(yīng)的socket。
tcp_hashinfo是一個(gè)散列表,用于存儲(chǔ)所有活動(dòng)的TCP套接字,從中查找與這個(gè)數(shù)據(jù)包對(duì)應(yīng)的sock(即struct sock實(shí)例)。如果找到了匹配的套接字,就說(shuō)明有一個(gè)連接正在接收這個(gè)數(shù)據(jù)包的源IP和端口發(fā)送的數(shù)據(jù)。
找到以后,首先要判斷socket是否已經(jīng)被用戶鎖定。
在Linux中,如果一個(gè) sock 對(duì)象被用戶進(jìn)程鎖定(例如,用戶進(jìn)程正在調(diào)用 recv 等系統(tǒng)調(diào)用讀取數(shù)據(jù)),那么內(nèi)核就不應(yīng)該直接修改 sock 的狀態(tài),而應(yīng)該將接收到的數(shù)據(jù)包放入后備處理隊(duì)列,稍后再處理(當(dāng)數(shù)據(jù)包被添加到后備處理隊(duì)列后,這個(gè)數(shù)據(jù)包的處理就結(jié)束了,只有當(dāng) socket 變?yōu)榭臻e狀態(tài),那些在后備處理隊(duì)列中的數(shù)據(jù)包才會(huì)被處理,通常這個(gè)處理過(guò)程就包括將數(shù)據(jù)包添加到接收隊(duì)列中)。
如果socket沒(méi)有被鎖定,則調(diào)用tcp_prequeue嘗試將數(shù)據(jù)包添加到sock的預(yù)處理隊(duì)列中。如果添加成功則返回1,否則返回0。
在這個(gè)函數(shù)里面,會(huì)對(duì) sysctl_tcp_low_latency 進(jìn)行判斷,也即是不是要低時(shí)延地處理網(wǎng)絡(luò)包。如果把 sysctl_tcp_low_latency 設(shè)置為 0,那就要放在 prequeue 隊(duì)列中暫存,這樣不用等待網(wǎng)絡(luò)包處理完畢,就可以離開(kāi)軟中斷的處理過(guò)程,但是會(huì)造成比較長(zhǎng)的時(shí)延(因?yàn)閿?shù)據(jù)包的處理延遲到了進(jìn)程被調(diào)度的時(shí)候。對(duì)于接收數(shù)據(jù)包而言沒(méi)有區(qū)別,因?yàn)槎际切枰竭M(jìn)程被調(diào)度時(shí)才拷貝到用戶空間。但是由于TCP協(xié)議處理被延遲,導(dǎo)致ACK的發(fā)送延遲,從而使數(shù)據(jù)發(fā)送端的數(shù)據(jù)發(fā)送延遲)。如果把 sysctl_tcp_low_latency 設(shè)置為 1,則調(diào)用 tcp_v4_do_rcv()立即處理。
實(shí)際上代碼中較新的版本已經(jīng)沒(méi)有了tcp_prequeue()函數(shù)。之所以取消prequeue,是因?yàn)樵诖蠖嗍褂檬录?qū)動(dòng)(epoll)的當(dāng)下,已經(jīng)很少有阻塞在recvfrom()或者read()的服務(wù)端代碼了。
如果數(shù)據(jù)包沒(méi)有加入預(yù)處理隊(duì)列則進(jìn)入接收的主體函數(shù)tcp_v4_do_rcv。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
if(sk->sk_state == TCP_ESTABLISHED) {
// 執(zhí)行鏈接狀態(tài)下的數(shù)據(jù)處理
if(tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
return 0;
}
// 其他非ESTABLISHED狀態(tài)的數(shù)據(jù)包處理
......
}
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, unsigned int len)
{
......
// 接收數(shù)據(jù)放到隊(duì)列中
eaten = tcp_queue_rcv(sk, skb, tcp_header_len, &fragstolen);
// 數(shù)據(jù)準(zhǔn)備好,喚醒socket上組色調(diào)的進(jìn)程
sk->sk_data_ready(sk, 0);
}
假設(shè)處理的是ESTABLISHED狀態(tài)下的包(即已經(jīng)完成握手,建立連接),這樣就又進(jìn)入了tcp_rcv_established函數(shù)進(jìn)行處理。
在tcp_rcv_established中完成了將接收到的數(shù)據(jù)放到socket的接收隊(duì)列尾部,并調(diào)用sk_data_ready來(lái)喚醒在socket上等待的用戶進(jìn)程(創(chuàng)建socket時(shí)在sock_init_data函數(shù)里將該指針設(shè)置成了sock_def_readable)。
**喚醒進(jìn)程時(shí),即使等待隊(duì)列里有多個(gè)進(jìn)程阻塞著,也只喚醒一個(gè)進(jìn)程,避免驚群。**會(huì)從頭部開(kāi)始依次檢查每一個(gè)進(jìn)程,看看是否滿足喚醒的條件。如果滿足條件,就將該進(jìn)程喚醒。
在等待隊(duì)列中,進(jìn)程是按照它們進(jìn)入隊(duì)列的順序排列的,即先進(jìn)入隊(duì)列的進(jìn)程在隊(duì)列的前面,后進(jìn)入隊(duì)列的進(jìn)程在隊(duì)列的后面
在前面調(diào)用recvfrom時(shí),當(dāng)數(shù)據(jù)不夠后調(diào)用的sk_wait_data函數(shù)中使用DEFINE_WAIT定義了等待隊(duì)列項(xiàng)的細(xì)節(jié),并且把curr->func設(shè)置成了autoremove_wake_function,那么在喚醒進(jìn)程時(shí)會(huì)去調(diào)用這個(gè)函數(shù),它會(huì)去調(diào)用default_wake_function將因?yàn)榈却蛔枞倪M(jìn)程喚醒。這個(gè)函數(shù)執(zhí)行完之后,這個(gè)進(jìn)程就可以就可以被推入可運(yùn)行隊(duì)列里,在這里又將產(chǎn)生一次進(jìn)程上下文切換的開(kāi)銷。
3)同步隊(duì)列阻塞總結(jié)
同步阻塞方式接收網(wǎng)絡(luò)包的整個(gè)過(guò)程分為兩個(gè)部分:
- 我們自己的代碼所在的進(jìn)程:我們調(diào)用的socket()函數(shù)會(huì)進(jìn)入內(nèi)核態(tài)創(chuàng)建必要的內(nèi)核對(duì)象。recv()函數(shù)會(huì)在進(jìn)入內(nèi)核態(tài)以后負(fù)責(zé)查看接收隊(duì)列,以及在沒(méi)有數(shù)據(jù)可以處理的時(shí)候把當(dāng)前進(jìn)程組色調(diào),讓出CPU。
- 硬中斷、軟中斷上下文:在這些組件中,將包處理完后會(huì)放到socket的接收隊(duì)列中,然后根據(jù)socket內(nèi)核對(duì)象找到其等待隊(duì)列中正在因?yàn)榈却蛔枞舻倪M(jìn)程,將它喚醒。
每次一個(gè)進(jìn)程專門(mén)為了等待一個(gè)socket上的數(shù)據(jù)就被從CPU上拿出來(lái),然后換上另一個(gè)進(jìn)程。等到數(shù)據(jù)準(zhǔn)備好,睡眠的進(jìn)程又會(huì)被喚醒,總共產(chǎn)生兩次進(jìn)程上下文切換開(kāi)銷。根據(jù)業(yè)界的測(cè)試,每一次切換大約花費(fèi)3-5微妙。
然而從開(kāi)發(fā)者的角度而言,進(jìn)程上下文切換其實(shí)沒(méi)有做有意義的工作。如果是網(wǎng)絡(luò)IO密集型的應(yīng)用,CPU就會(huì)被迫不停地做進(jìn)程切換這種無(wú)用功。
這種模式在客戶端角色上現(xiàn)在還存在使用的情形,因?yàn)槟愕倪M(jìn)程可能確實(shí)需要等MySQL的數(shù)據(jù)返回成功之后才能渲染頁(yè)面返回給用戶,否則什么也干不了。
而在服務(wù)端角色上,這種模式完全無(wú)法使用。因?yàn)檫@種模型里的socket和進(jìn)程是一對(duì)一的,現(xiàn)在的單臺(tái)機(jī)器要承載成千上萬(wàn)甚至更多的用戶連接請(qǐng)求,如果用上面的方式,就得為每個(gè)用戶請(qǐng)求都創(chuàng)建一個(gè)進(jìn)程,否則無(wú)法同時(shí)處理多個(gè)用戶的請(qǐng)求,然而這肯定是不現(xiàn)實(shí)的。
所以我們需要更高效的網(wǎng)絡(luò)IO模型!可前往深入理解Linux內(nèi)核網(wǎng)絡(luò)——內(nèi)核與用戶進(jìn)程協(xié)作之多路復(fù)用方案(epoll)繼續(xù)學(xué)習(xí)多路IO復(fù)用解決方案~
參考資料:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-539691.html
《深入理解Linux網(wǎng)絡(luò)》—— 張彥飛文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-539691.html
到了這里,關(guān)于深入理解Linux內(nèi)核網(wǎng)絡(luò)——內(nèi)核與用戶進(jìn)程協(xié)作之同步阻塞方案(BIO)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!