TCP的發(fā)送系列 — 發(fā)送緩存的管理(二)
TCP的發(fā)送緩存管理發(fā)生在兩個層面上:單個Socket和整個TCP層。
上一篇blog講述了單個Socket層面上的發(fā)送緩存管理,現(xiàn)在來看下整個TCP層面上的發(fā)送緩存管理。
從TCP層面判斷發(fā)送緩存的申請是否合法
在申請發(fā)送緩存時,會調(diào)用sk_stream_memory_free()來判斷sock發(fā)送隊(duì)列的大小是否超過
了sock發(fā)送緩存的上限,如果超過了,就要進(jìn)入睡眠來等待sock的發(fā)送緩存可寫事件。
這是從單個socket層面來判斷是否允許分配發(fā)送緩存。
在調(diào)用sk_stream_alloc_skb()申請完發(fā)送緩存后,還要從TCP層面來判斷此次的申請是否合法。
如果不合法,就使用__kfree_skb()來釋放申請好的skb??梢姲l(fā)送緩存的申請,需要經(jīng)過兩重關(guān)卡。
從TCP層面來判斷發(fā)送緩存的申請是否合法,需要考慮整個TCP層面的內(nèi)存使用量,以及此socket
的發(fā)送緩存使用量。sk->sk_forward_alloc為sock預(yù)分配緩存的大小,是sock事先分配好還未使用的內(nèi)存。
當(dāng)申請新的發(fā)送緩存后,如果發(fā)現(xiàn)sk->sk_forward_alloc < skb->truesize,即預(yù)分配緩存用光了,
才需要調(diào)用sk_wme_schedule()來從TCP層面判斷合法性,否則不用再做檢查。
[java] static inline bool sk_wmem_schedule(struct sock *sk, int size)
{ /* TCP層是有統(tǒng)計內(nèi)存使用的,所以條件為假 */ if (! sk_has_account(sk)) return true; /* 如果本次使用的內(nèi)存skb->truesize,少于sk預(yù)分配且未使用的緩存的大小,那么不用進(jìn)行 * 進(jìn)一步檢查。否則需要從TCP層面判斷此次發(fā)送緩存的申請是否合法。 */ return size <= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_SEND); } static inline bool sk_has_account(struct sock *sk) { /* return ture if protocol supports memory accounting */ return !! sk->sk_prot->memory_allocated; } /* return minimum truesize of one skb containing X bytes of data */ #define SKB_TRUESIZE(X) ((X) + \ SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \ SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
__sk_mem_schedule()用來從TCP層面判斷此次發(fā)送緩存的申請是否合法,如果是合法的,
會更新預(yù)分配緩存sk->sk_forward_alloc和TCP層總的內(nèi)存使用量tcp_memory_allocated,
后者的單位為頁。
Q:哪些情況下此次發(fā)送緩存的申請是合法的呢?
1. TCP層的內(nèi)存使用量低于最小值sysctl_tcp_mem[0]。
2. sock的發(fā)送緩存使用量低于最小值sysctl_tcp_wmem[0]。
3. TCP層不處于內(nèi)存壓力狀態(tài),即TCP層的內(nèi)存使用量低于sysctl_tcp_wmem[1]。
4. TCP層處于內(nèi)存壓力狀態(tài),但當(dāng)前socket使用的內(nèi)存還不是太高。
5. TCP層的內(nèi)存使用量超過最大值sysctl_tcp_wmem[2],降低發(fā)送緩存的上限后,發(fā)送隊(duì)列的總大小超過
了發(fā)送緩存的上限了。因此之后會進(jìn)入睡眠等待,所以也判為合法的。
可以看到,在絕大多數(shù)情況下發(fā)送緩存的申請都是合法的,除非TCP的內(nèi)存使用量已經(jīng)到極限了。
除了判斷此次發(fā)送緩存申請的合法性,__sk_mem_schedule()還做了如下事情:
1. 如果TCP的內(nèi)存使用量低于最小值sysctl_tcp_mem[0],就清零TCP的內(nèi)存壓力標(biāo)志tcp_memory_pressure。
2. 如果TCP的內(nèi)存使用量高于壓力值sysclt_tcp_mem[1],把TCP的內(nèi)存壓力標(biāo)志tcp_memory_pressure置為1。
3. 如果TCP的內(nèi)存使用量高于最大值sysctl_tcp_mem[2],就減小sock發(fā)送緩存的上限sk->sk_sndbuf。
返回值為1時,表示發(fā)送緩存的申請是合法的;返回值為0時,表示不合法。
[java] /* increase sk_forward_alloc and memory_allocated
* @sk: socket * @size: memory size to allocate * @kind: allocation type * If kind is SK_MEM_SEND, it means wmem allocation. * Otherwise it means rmem allocation. This function assumes that * protocols which have memory pressure use sk_wmem_queued as * write buffer accounting. */ int __sk_mem_schedule(struct sock *sk, int size, int kind) { struct proto *prot = sk->sk_prot; /* 實(shí)例為tcp_prot */ int amt = sk_mem_pages(size); /* 把size轉(zhuǎn)換為頁數(shù),向上取整 */ long allocated; int parent_status = UNDER_LIMIT; sk->sk_forward_alloc += amt * SK_MEM_QUANTUM; /* 更新預(yù)分配緩存的大小 */ /* 更新后的TCP內(nèi)存使用量tcp_memory_allocated,單位為頁 */ allocated = sk_memory_allocated_add(sk, amt, &parent_status); /* Under limit. 如果TCP的內(nèi)存使用量低于最小值sysctl_tcp_mem[0] */ if (parent_status == UNDER_LIMIT && allocated <= sk_prot_mem_limits(sk, 0)) { sk_leave_memory_pressure(sk); /* 清零TCP層的內(nèi)存壓力標(biāo)志tcp_memory_pressure */ return 1; } /* Under pressure. (we or our parents). * 如果TCP的內(nèi)存使用量高于壓力值sysclt_tcp_mem[1],把TCP層的內(nèi)存壓力標(biāo)志 * tcp_memory_pressure置為1。 */ if ((parent_status > SOFT_LIMIT) || allocated > sk_prot_mem_limits(sk, 1)) sk_enter_memory_pressure(sk); /* Over hard limit (we or our parents). * 如果TCP層的內(nèi)存使用量高于最大值sysctl_tcp_mem[2],就減小sock發(fā)送緩存的上限 * sk->sk_sndbuf。 */ if ((parent_status == OVER_LIMIT || (allocated > sk_prot_mem_limits(sk, 2))) goto suppress_allocation; /* guarantee minimum buffer size under pressure */ /* 不管是在發(fā)送還是接收時,都要保證sock至少有sysctl_tcp_{r,w}mem[0]的內(nèi)存可用 */ if (kind == SK_MEM_RECV) { if (atomic_read(&sk->sk_rmem_alloc) < prot->sysctl_rmem[0]) return 1; } else { /* SK_MEM_SEND */ if (sk->sk_type == SOCK_STREAM) { if (sk->sk_wmem_queued < prot->sysctl_wmem[0]) return 1; } else if (atomic_read(&sk->sk_wmem_alloc) < prot->sysctl_wmem[0]) return 1; } if (sk_has_memory_pressure(sk)) { int alloc; /* 如果TCP不處于內(nèi)存壓力狀態(tài),直接返回 */ if (! sk_under_memory_pressure(sk)) return 1; alloc = sk_sockets_allocated_read_positive(sk); /* 當(dāng)前使用TCP的socket個數(shù) */ /* 如果當(dāng)前socket使用的內(nèi)存還不是太高時,返回真 */ if (sk_prot_mem_limits(sk, 2) > alloc * sk_mem_pages(sk->sk_wmem_queued + atomic_read(&sk->sk_rmem_alloc) + sk->sk_forward_alloc)) return 1; } suppress_allocation: if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) { /* 減小sock發(fā)送緩沖區(qū)的上限,使得sndbuf不超過發(fā)送隊(duì)列總大小的一半, * 不低于兩個數(shù)據(jù)包的MIN_TRUESIZE。 */ sk_stream_moderate_sndbuf(sk); /* Fail only if socket is under its sndbuf. * In this case we cannot block, so that we have to fail. */ if (sk->sk_wmem_queued + size >= sk->sk_sndbuf) return 1; } trace_sock_exceed_buf_limit(sk, prot, allocated); /* 走到這里,判定此次發(fā)送緩存的申請為不合法的,撤銷之前的內(nèi)存計數(shù)更新 */ /* Alas. Undo changes. */ sk->sk_forward_alloc -= amt * SK_MEM_QUANTUM; sk_memory_allocated_sub(sk, amt); return 0; } /* 把字節(jié)數(shù)amt轉(zhuǎn)換為頁數(shù),向上取整 */ static inline int sk_mem_pages(int amt) { return (amt + SK_MEM_QUANTUM - 1) >> SK_MEM_QUANTUM_SHIFT; } #define SK_MEM_QUANTUM ((int) PAGE_SIZE) /* 返回更新后的TCP內(nèi)使用量tcp_memory_allocated,單位為頁 */ static inline long sk_memory_allocated_add(struct sock *sk, int amt, int *parent_status) { struct proto *prot = sk->sk_prot; /* Cgroup相關(guān),此處略過 */ if (mem_cgroup_sockets_enabled && sk->sk_cgrp) { ... } return atomic_long_add_return(amt, prot->memory_allocated); }
#p#
sysctl_tcp_mem[0]:最小值
sysctl_tcp_mem[1]:壓力值
sysctl_tcp_mem[2]:最大值
[java] static inline long sk_prot_mem_limits(const struct sock *sk, int index)
{ long *prot = sk->sk_prot->sysctl_mem; /* Cgroup相關(guān) */ if (mem_cgroup_sockets_enabled && sk->sk_cgrp) prot = sk->sk_cgrp->sysctl_mem; return prot[index]; }
因缺少發(fā)送緩存而睡眠等待
在tcp_sendmsg()中,如果發(fā)送隊(duì)列的總大小sk_wmem_queued大于等于發(fā)送緩存的上限sk_sndbuf,
或者發(fā)送緩存中尚未發(fā)送的數(shù)據(jù)量超過了用戶的設(shè)置值,就進(jìn)入睡眠等待。
如果申請發(fā)送緩存失敗了,也會進(jìn)行睡眠等待。
(1) 判斷條件
sk_stream_memory_free()用來判斷sock是否有剩余的發(fā)送緩存。
[java] static inline bool sk_stream_memory_free(const struct sock *sk)
{ if (sk->sk_wmem_queued >= sk->sk_sndbuf) return false; return sk->sk_prot->stream_memory_free ? sk->sk_prot->stream_memory_free(sk) : true; } static inline bool tcp_stream_memory_free(const struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); u32 notsent_bytes = tp->write_seq - tp->snd_nxt; /* 尚未發(fā)送的數(shù)據(jù)大小 */ /* 當(dāng)尚未發(fā)送的數(shù)據(jù),少于配置的值時,才返回真。 * 這是為了避免發(fā)送緩存占用過多的內(nèi)存。 */ return notsent_bytes < tcp_notsent_lowat(tp); }
如果有使用TCP_NOTSENT_LOWAT選項(xiàng),則使用用戶設(shè)置的值。
否則使用sysctl_tcp_notsent_lowat,默認(rèn)為無窮大。
[java] static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{ return tp->notsent_lowat ?: sysctl_tcp_notsent_lowat; }
(2) 睡眠等待
如果發(fā)送隊(duì)列的總大小sk_wmem_queued大于等于發(fā)送緩存的上限sk_sndbuf,
或者發(fā)送緩存中尚未發(fā)送的數(shù)據(jù)量超過了用戶的設(shè)置,就進(jìn)入等待。
如果因?yàn)門CP層的內(nèi)存不足,導(dǎo)致申請發(fā)送緩存失敗了,也會進(jìn)行睡眠等待。
Q:需要睡眠等待多長的時間呢?
需要分兩種情況:
1. 等待的原因是TCP層的內(nèi)存不足。
剛進(jìn)入函數(shù)時,會判斷sock的發(fā)送緩存是否達(dá)到了上限。
如果此時sock尚有發(fā)送緩存額度,說明是TCP層內(nèi)存不足導(dǎo)致發(fā)送緩存申請失敗的,
設(shè)置等待時間為一個2~202ms的偽隨機(jī)數(shù),超時后就結(jié)束等待。
2. 等待的原因是sock的發(fā)送緩存不足。
在睡眠的過程中,當(dāng)有可用的發(fā)送緩存時,進(jìn)程會被喚醒,從而結(jié)束等待。
否則達(dá)到超時時間后,返回錯誤。
[java] /* Wait for more memory for a socket
* @sk: socket to wait for memory * @timeo_p: for how long */ int sk_stream_wait_memory(struct sock *sk, long *timeo_p) { int err = 0; long vm_wait = 0; long current_timeo = *timeo_p; DEFINE_WAIT(wait); /* 初始化等待任務(wù) */ /* 如果sock還有發(fā)送緩存額度,說明是TCP層內(nèi)存不足導(dǎo)致的。 * 初始化等待時間為一個2~202ms的偽隨機(jī)數(shù)。 */ if (sk_stream_memory_free(sk)) current_timeo = vm_wait = (prandom_u32() % (HZ / 5)) + 2; while (1) { /* 設(shè)置異步發(fā)送時,發(fā)送緩存不夠的標(biāo)志 */ set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags); /* 把等待任務(wù)加入到socket等待隊(duì)列頭部,把進(jìn)程的狀態(tài)設(shè)為TASK_INTERRUPTIBLE */ prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); /* 如果連接有錯誤,或者不允許發(fā)送數(shù)據(jù)了,那么返回-EPIPE */ if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN)) goto do_error; /* 如果是非阻塞的,或者等待超時了,返回-EAGAIN */ if (! *timeo_p) goto do_nonblock; /* 如果進(jìn)程有待處理的信號,如果沒有設(shè)置超時時間返回-ERESTARTSYS, * 否則返回-EINTR. */ if (signal_pending(current)) goto do_interrupte; clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags); /* 如果sock已經(jīng)有可用的發(fā)送緩存了。并滿足以下任一條件: * 1. 此次等待是由于sock的發(fā)送緩存不足。 * 2. 此次等待是由于TCP層內(nèi)存不足,經(jīng)過了一次睡眠vm_wait設(shè)為0。 */ if (sk_stream_memory_free(sk) && ! vm_wait) break; set_bit(SOCK_NOSPACE, &sk->sk_socket->flags); sk->sk_write_pending++; /* 進(jìn)入睡眠等待 */ sk_wait_event(sk, ¤tt_timeo, sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN) || (sk_stream_memory_free(sk) && ! vm_wait)); sk->sk_write_pending--; /* 如果vm_wait不為0,睡眠2~202ms后,就把vm_wait清零了 */ if (vm_wait) { vm_wait -= current_timeo; current_timo = *timeo_p; if (current_timeo != MAX_SCHEDULE_TIMEOUT && (current_timeo -= vm_wait) < 0) current_timeo = 0; vm_wait = 0; } *timeo_p = current_timeo; /* 更新發(fā)送的超時等待時間 */ } out: /* 把等待任務(wù)從等待隊(duì)列中刪除,把當(dāng)前進(jìn)程的狀態(tài)設(shè)為TASK_RUNNING */ finish_wait(sk_sleep(sk), &wait); return err; do_error: err = -EPIPE; goto out; do_nonblock: err = -EAGAIN; goto out; do_interrupted: err = sock_intr_errno(*timeo_p); goto out; }
#p#
因有發(fā)送緩存可寫事件而被喚醒
sk->sk_write_space的實(shí)例為sock_def_write_space()。
如果socket是SOCK_STREAM類型的,那么函數(shù)指針的值會更新為sk_stream_write_space()。
sk_stream_write_space()在TCP中的調(diào)用路徑為:
tcp_rcv_established / tcp_rcv_state_process
tcp_data_snd_check
tcp_check_space
tcp_new_space
[java] static void tcp_check_space(struct sock *sk)
{ /* 如果發(fā)送隊(duì)列中有skb被釋放了 */ if (sock_flag(sk, SOCK_QUEUE_SHRUNK)) { sock_reset_flag(sk, SOCK_QUEUE_SHRUNK); /* 如果設(shè)置了同步發(fā)送時,發(fā)送緩存不足的標(biāo)志 */ if (sk->sk_socket && test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) tcp_new_space(sk); /* 更新發(fā)送緩存 */ } }
[java] /* When incoming ACK allowed to free some skb from write_queue, * we remember this event in flag SOCK_QUEUE_SHRUNK and wake up socket * on the exit from tcp input handler. */ static void tcp_new_space(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); if (tcp_should_expand_sndbuf(sk)) { tcp_sndbuf_expand(sk); tp->snd_cwnd_stamp = tcp_time_stamp; } /* 檢查是否需要觸發(fā)有緩存可寫事件 */ sk->sk_write_space(sk); }
[java] void sk_stream_write_space(struct sock *sk) { struct socket *sock = sk->sk_socket; struct socket_wq *wq; /* 等待隊(duì)列和異步通知隊(duì)列 */ /* 如果剩余的發(fā)送緩存不低于發(fā)送緩存上限的1/3,且尚未發(fā)送的數(shù)據(jù)不高于一定值時 */ if (sk_stream_is_writeable(sk) && sock) { clear_bit(SOCK_NOSPACE, &sock->flags); /* 清除發(fā)送緩存不夠的標(biāo)志 */ rcu_read_lock(); wq = rcu_dereference(sk->sk_wq); /* socket的等待隊(duì)列和異步通知隊(duì)列 */ if (wq_has_sleeper(wq)) /* 如果等待隊(duì)列不為空,則喚醒一個睡眠進(jìn)程 */ wake_up_interruptible_poll(&wq->wait, POLLOUT | POLLWRNORM | POLLWRBAND); /* 異步通知隊(duì)列不為空,且允許發(fā)送數(shù)據(jù)時。 * 檢測sock的發(fā)送隊(duì)列是否曾經(jīng)到達(dá)上限,如果有的話發(fā)送SIGIO信號,告知異步通知隊(duì)列上 * 的進(jìn)程有發(fā)送緩存可寫。 */ if (wq && wq->fasync_list && !(sk->sk_shutdown & SEND_SHUTDOWN)) sock_wake_async(sock, SOCK_WAKE_SPACE, POLL_OUT); rcu_read_unlock(); } } #define wake_up_interruptible_poll(x, m) \ __wake_up(x, TASK_INTERRUPTIBLE, 1, (void *) (m))
如果剩余的發(fā)送緩存大于發(fā)送緩存上限的1/3,且尚未發(fā)送的數(shù)據(jù)少于一定值時,才會觸發(fā)有發(fā)送
緩存可寫的事件。
[java] view plaincopystatic inline bool sk_stream_is_writeable(const struct sock *sk) { return sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) && sk_stream_memory_free(sk); } static inline int sk_stream_wspace(const struct sock *sk) { return sk->sk_sndbuf - sk->sk_wmem_queued; } static inline int sk_stream_min_wspace(const struct sock *sk) { return sk->sk_wmem_queued >> 1; } 檢查尚未發(fā)送的數(shù)據(jù)是否已經(jīng)夠多了,如果超過了用戶設(shè)置的值,就不用觸發(fā)有發(fā)送緩存可寫事件, 以免使用過多的內(nèi)存。 [java] static inline bool sk_stream_memory_free(const struct sock *sk)
{ if (sk->sk_wmem_queued >= sk->sk_sndbuf) return false; return sk->sk_prot->stream_memory_free ? sk->sk_prot->stream_memory_free(sk) : true; } static inline bool tcp_stream_memory_free(const struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); u32 notsent_bytes = tp->write_seq - tp->snd_nxt; /* 尚未發(fā)送的數(shù)據(jù)大小 */ /* 當(dāng)尚未發(fā)送的數(shù)據(jù),少于配置的值時,才觸發(fā)有發(fā)送緩存可寫的事件。 * 這是為了避免發(fā)送緩存占用過多的內(nèi)存。 */ return notsent_bytes < tcp_notsent_lowat(tp); }
如果有使用TCP_NOTSENT_LOWAT選項(xiàng),則使用用戶設(shè)置的值。
否則使用sysctl_tcp_notsent_lowat,默認(rèn)為無窮大。
[java] static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{ return tp->notsent_lowat ?: sysctl_tcp_notsent_lowat; }