国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

圖解 | 深入理解高性能網絡開發路上的絆腳石 - 同步阻塞網絡 IO

網絡 通信技術
在網絡開發模型中,有一種非常易于開發同學使用的方式,那就是同步阻塞的網絡 IO(在 Java 中習慣叫 BIO)。

[[386495]]

本文轉載自微信公眾號「開發內功修煉」,作者張彥飛allen 。轉載本文請聯系開發內功修煉公眾號。

在網絡開發模型中,有一種非常易于開發同學使用的方式,那就是同步阻塞的網絡 IO(在 Java 中習慣叫 BIO)。

例如我們想請求服務器上的一段數據,那么 C 語言的一段代碼 demo 大概是下面這樣:

  1. int main() 
  2.  int sk = socket(AF_INET, SOCK_STREAM, 0); 
  3.  connect(sk, ...) 
  4.  recv(sk, ...) 

但是在高并發的服務器開發中,這種網絡 IO 的性能奇差。因為

1.進程在 recv 的時候大概率會被阻塞掉,導致一次進程切換

2.當連接上數據就緒的時候進程又會被喚醒,又是一次進程切換

3.一個進程同時只能等待一條連接,如果有很多并發,則需要很多進程

如果用一句話來概括,那就是:同步阻塞網絡 IO 是高性能網絡開發路上的絆腳石! 俗話說得好,知己知彼方能百戰百勝。所以我們今天先不講優化,只深入分析同步阻塞網絡 IO 的內部實現。

在上面的 demo 中雖然只是簡單的兩三行代碼,但實際上用戶進程和內核配合做了非常多的工作。先是用戶進程發起創建 socket 的指令,然后切換到內核態完成了內核對象的初始化。接下來 Linux 在數據包的接收上,是硬中斷和 ksoftirqd 進程在進行處理。當 ksoftirqd 進程處理完以后,再通知到相關的用戶進程。

從用戶進程創建 socket,到一個網絡包抵達網卡到被用戶進程接收到,總體上的流程圖如下:

 

我們今天用圖解加源碼分析的方式來詳細拆解一下上面的每一個步驟,來看一下在內核里是它們是怎么實現的。閱讀完本文,你將深刻地理解在同步阻塞的網絡 IO 性能低下的原因!

一、創建一個 socket

開篇源碼中的 socket 函數調用執行完以后,內核在內部創建了一系列的 socket 相關的內核對象(是的,不是只有一個)。它們互相之間的關系如圖。當然了,這個對象比圖示的還要更復雜。我只在圖中把和今天的主題相關的內容展現了出來。

 

我們來翻翻源碼,看下上面的結構是如何被創造出來的。

  1. //file:net/socket.c 
  2. SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 
  3.  ...... 
  4.  retval = sock_create(family, type, protocol, &sock); 

sock_create 是創建 socket 的主要位置。其中 sock_create 又調用了 __sock_create。

  1. //file:net/socket.c 
  2. int __sock_create(struct net *net, int family, int type, int protocol, 
  3.     struct socket **res, int kern) 
  4.  struct socket *sock; 
  5.  const struct net_proto_family *pf; 
  6.  
  7.  ...... 
  8.  
  9.  //分配 socket 對象 
  10.  sock = sock_alloc(); 
  11.  
  12.  //獲得每個協議族的操作表 
  13.  pf = rcu_dereference(net_families[family]); 
  14.  
  15.  //調用每個協議族的創建函數, 對于 AF_INET 對應的是 
  16.  err = pf->create(net, sock, protocol, kern); 

在 __sock_create 里,首先調用 sock_alloc 來分配一個 struct sock 對象。接著在獲取協議族的操作函數表,并調用其 create 方法。對于 AF_INET 協議族來說,執行到的是 inet_create 方法。

  1. //file:net/ipv4/af_inet.c 
  2. tatic int inet_create(struct net *net, struct socket *sock, int protocol, 
  3.          int kern) 
  4.  struct sock *sk; 
  5.  
  6.  //查找對應的協議,對于TCP SOCK_STREAM 就是獲取到了 
  7.  //static struct inet_protosw inetsw_array[] = 
  8.     //{ 
  9.  //    { 
  10.  //     .type =       SOCK_STREAM, 
  11.  //     .protocol =   IPPROTO_TCP, 
  12.  //     .prot =       &tcp_prot, 
  13.  //     .ops =        &inet_stream_ops, 
  14.  //     .no_check =   0, 
  15.  //     .flags =      INET_PROTOSW_PERMANENT | 
  16.  //            INET_PROTOSW_ICSK, 
  17.  //    }, 
  18.  //} 
  19.     list_for_each_entry_rcu(answer, &inetsw[sock->type], list) { 
  20.  
  21.  //將 inet_stream_ops 賦到 socket->ops 上  
  22.  sock->ops = answer->ops; 
  23.  
  24.  //獲得 tcp_prot 
  25.  answer_prot = answer->prot; 
  26.  
  27.  //分配 sock 對象, 并把 tcp_prot 賦到 sock->sk_prot 上 
  28.  sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot); 
  29.  
  30.  //對 sock 對象進行初始化 
  31.  sock_init_data(sock, sk); 

在 inet_create 中,根據類型 SOCK_STREAM 查找到對于 tcp 定義的操作方法實現集合 inet_stream_ops 和 tcp_prot。并把它們分別設置到 socket->ops 和 sock->sk_prot 上。

 

我們再往下看到了 sock_init_data。在這個方法中將 sock 中的 sk_data_ready 函數指針進行了初始化,設置為默認 sock_def_readable()。

  1. //file: net/core/sock.c 
  2. void sock_init_data(struct socket *sock, struct sock *sk)  
  3.     sk->sk_data_ready   =   sock_def_readable; 
  4.     sk->sk_write_space  =   sock_def_write_space; 
  5.     sk->sk_error_report =   sock_def_error_report; 

當軟中斷上收到數據包時會通過調用 sk_data_ready 函數指針(實際被設置成了 sock_def_readable()) 來喚醒在 sock 上等待的進程。這個咱們后面介紹軟中斷的時候再說,這里記住這個就行了。

至此,一個 tcp對象,確切地說是 AF_INET 協議族下 SOCK_STREAM對象就算是創建完成了。這里花費了一次 socket 系統調用的開銷

二、等待接收消息

接著我們來看 recv 函數依賴的底層實現。首先通過 strace 命令跟蹤,可以看到 clib 庫函數 recv 會執行到 recvfrom 系統調用。

進入系統調用后,用戶進程就進入到了內核態,通過執行一系列的內核協議層函數,然后到 socket 對象的接收隊列中查看是否有數據,沒有的話就把自己添加到 socket 對應的等待隊列里。最后讓出CPU,操作系統會選擇下一個就緒狀態的進程來執行。整個流程圖如下:

 

看完了整個流程圖,接下來讓我們根據源碼來看更詳細的細節。其中我們今天要關注的重點是 recvfrom 最后是怎么把自己的進程給阻塞掉的(假如我們沒有使用 O_NONBLOCK 標記)。

  1. //file: net/socket.c 
  2. SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size
  3.   unsigned int, flags, struct sockaddr __user *, addr, 
  4.   int __user *, addr_len) 
  5.  struct socket *sock; 
  6.  
  7.  //根據用戶傳入的 fd 找到 socket 對象 
  8.  sock = sockfd_lookup_light(fd, &err, &fput_needed); 
  9.  ...... 
  10.  err = sock_recvmsg(sock, &msg, size, flags); 
  11.  ...... 

sock_recvmsg ==> __sock_recvmsg => __sock_recvmsg_nosec

  1. static inline int __sock_recvmsg_nosec(struct kiocb *iocb, struct socket *sock, 
  2.            struct msghdr *msg, size_t sizeint flags) 
  3.  ...... 
  4.  return sock->ops->recvmsg(iocb, sock, msg, size, flags); 

調用 socket 對象 ops 里的 recvmsg, 回憶我們上面的 socket 對象圖,從圖中可以看到 recvmsg 指向的是 inet_recvmsg 方法。

  1. //file: net/ipv4/af_inet.c 
  2. int inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, 
  3.    size_t sizeint flags) 
  4.  ... 
  5.  
  6.  err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT, 
  7.        flags & ~MSG_DONTWAIT, &addr_len); 

這里又遇到一個函數指針,這次調用的是 socket 對象里的 sk_prot 下面的 recvmsg方法。同上,得出這個 recvmsg 方法對應的是 tcp_recvmsg 方法。

  1. //file: net/ipv4/tcp.c 
  2. int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, 
  3.   size_t len, int nonblock, int flags, int *addr_len) 
  4.  int copied = 0; 
  5.  ... 
  6.  do { 
  7.   //遍歷接收隊列接收數據 
  8.   skb_queue_walk(&sk->sk_receive_queue, skb) { 
  9.    ... 
  10.   } 
  11.   ... 
  12.  } 
  13.  
  14.  if (copied >= target) { 
  15.   release_sock(sk); 
  16.   lock_sock(sk); 
  17.  } else //沒有收到足夠數據,啟用 sk_wait_data 阻塞當前進程 
  18.   sk_wait_data(sk, &timeo); 

終于看到了我們想要看的東西,skb_queue_walk 是在訪問 sock 對象下面的接收隊列了。

 

如果沒有收到數據,或者收到不足夠多,則調用 sk_wait_data 把當前進程阻塞掉。

  1. //file: net/core/sock.c 
  2. int sk_wait_data(struct sock *sk, long *timeo) 
  3.  //當前進程(current)關聯到所定義的等待隊列項上 
  4.  DEFINE_WAIT(wait); 
  5.  
  6.  // 調用 sk_sleep 獲取 sock 對象下的 wait 
  7.  // 并準備掛起,將進程狀態設置為可打斷 INTERRUPTIBLE 
  8.  prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); 
  9.  set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags); 
  10.  
  11.  // 通過調用schedule_timeout讓出CPU,然后進行睡眠 
  12.  rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue)); 
  13.  ... 

我們再來詳細看下 sk_wait_data 是怎么把當前進程給阻塞掉的。

 

首先在 DEFINE_WAIT 宏下,定義了一個等待隊列項 wait。在這個新的等待隊列項上,注冊了回調函數 autoremove_wake_function,并把當前進程描述符 current 關聯到其 .private成員上。

  1. //file: include/linux/wait.h 
  2. #define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function) 
  3.  
  4. #define DEFINE_WAIT_FUNC(namefunction)    \ 
  5.  wait_queue_t name = {      \ 
  6.   .private = current,    \ 
  7.   .func  = function,    \ 
  8.   .task_list = LIST_HEAD_INIT((name).task_list), \ 
  9.  } 

緊接著在 sk_wait_data 中 調用 sk_sleep 獲取 sock 對象下的等待隊列列表頭 wait_queue_head_t。sk_sleep 源代碼如下:

  1. //file: include/net/sock.h 
  2. static inline wait_queue_head_t *sk_sleep(struct sock *sk) 
  3.  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0); 
  4.  return &rcu_dereference_raw(sk->sk_wq)->wait; 

接著調用 prepare_to_wait 來把新定義的等待隊列項 wait 插入到 sock 對象的等待隊列下。

  1. //file: kernel/wait.c 
  2. void 
  3. prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state) 
  4.  unsigned long flags; 
  5.  
  6.  wait->flags &= ~WQ_FLAG_EXCLUSIVE; 
  7.  spin_lock_irqsave(&q->lock, flags); 
  8.  if (list_empty(&wait->task_list)) 
  9.   __add_wait_queue(q, wait); 
  10.  set_current_state(state); 
  11.  spin_unlock_irqrestore(&q->lock, flags); 

這樣后面當內核收完數據產生就緒時間的時候,就可以查找 socket 等待隊列上的等待項,進而就可以找到回調函數和在等待該 socket 就緒事件的進程了。

最后再調用 sk_wait_event 讓出 CPU,進程將進入睡眠狀態,這會導致一次進程上下文的開銷。

接下來的小節里我們將能看到進程是如何被喚醒的了。

三、軟中斷模塊

接著我們再轉換一下視角,來看負責接收和處理數據包的軟中斷這邊。關于網絡包到網卡后是怎么被網卡接收,最后在交由軟中斷處理的,這里就不多贅述了。感興趣的請看之前的文章《圖解Linux網絡包接收過程》。我們今天直接從 tcp 協議的接收函數 tcp_v4_rcv 看起。

 

軟中斷(也就是 Linux 里的 ksoftirqd 進程)里收到數據包以后,發現是 tcp 的包的話就會執行到 tcp_v4_rcv 函數。接著走,如果是 ESTABLISH 狀態下的數據包,則最終會把數據拆出來放到對應 socket 的接收隊列中。然后調用 sk_data_ready 來喚醒用戶進程。

我們看更詳細一點的代碼:

  1. // file: net/ipv4/tcp_ipv4.c 
  2. int tcp_v4_rcv(struct sk_buff *skb) 
  3.  ...... 
  4.  th = tcp_hdr(skb); //獲取tcp header 
  5.  iph = ip_hdr(skb); //獲取ip header 
  6.  
  7.  //根據數據包 header 中的 ip、端口信息查找到對應的socket 
  8.  sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); 
  9.  ...... 
  10.  
  11.  //socket 未被用戶鎖定 
  12.  if (!sock_owned_by_user(sk)) { 
  13.   { 
  14.    if (!tcp_prequeue(sk, skb)) 
  15.     ret = tcp_v4_do_rcv(sk, skb); 
  16.   } 
  17.  } 

在 tcp_v4_rcv 中首先根據收到的網絡包的 header 里的 source 和 dest 信息來在本機上查詢對應的 socket。找到以后,我們直接進入接收的主體函數 tcp_v4_do_rcv 來看。

  1. //file: net/ipv4/tcp_ipv4.c 
  2. int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb) 
  3.  if (sk->sk_state == TCP_ESTABLISHED) {  
  4.  
  5.   //執行連接狀態下的數據處理 
  6.   if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) { 
  7.    rsk = sk; 
  8.    goto reset; 
  9.   } 
  10.   return 0; 
  11.  } 
  12.  
  13.  //其它非 ESTABLISH 狀態的數據包處理 
  14.  ...... 

我們假設處理的是 ESTABLISH 狀態下的包,這樣就又進入 tcp_rcv_established 函數中進行處理。

  1. //file: net/ipv4/tcp_input.c 
  2. int tcp_rcv_established(struct sock *sk, struct sk_buff *skb, 
  3.    const struct tcphdr *th, unsigned int len) 
  4.  ...... 
  5.  
  6.  //接收數據到隊列中 
  7.  eaten = tcp_queue_rcv(sk, skb, tcp_header_len, 
  8.             &fragstolen); 
  9.  
  10.  //數據 ready,喚醒 socket 上阻塞掉的進程 
  11.  sk->sk_data_ready(sk, 0); 

在 tcp_rcv_established 中通過調用 tcp_queue_rcv 函數中完成了將接收數據放到 socket 的接收隊列上。

 

如下源碼所示

  1. //file: net/ipv4/tcp_input.c 
  2. static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen, 
  3.     bool *fragstolen) 
  4.  //把接收到的數據放到 socket 的接收隊列的尾部 
  5.  if (!eaten) { 
  6.   __skb_queue_tail(&sk->sk_receive_queue, skb); 
  7.   skb_set_owner_r(skb, sk); 
  8.  } 
  9.  return eaten; 

調用 tcp_queue_rcv 接收完成之后,接著再調用 sk_data_ready 來喚醒在socket上等待的用戶進程。 這又是一個函數指針。回想上面我們在 創建 socket 流程里執行到的 sock_init_data 函數,在這個函數里已經把 sk_data_ready 設置成 sock_def_readable 函數了(可以ctrl + f 搜索前文)。它是默認的數據就緒處理函數。

  1. //file: net/core/sock.c 
  2. static void sock_def_readable(struct sock *sk, int len) 
  3.  struct socket_wq *wq; 
  4.  
  5.  rcu_read_lock(); 
  6.  wq = rcu_dereference(sk->sk_wq); 
  7.  
  8.  //有進程在此 socket 的等待隊列 
  9.  if (wq_has_sleeper(wq)) 
  10.   //喚醒等待隊列上的進程 
  11.   wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI | 
  12.       POLLRDNORM | POLLRDBAND); 
  13.  sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN); 
  14.  rcu_read_unlock(); 

在 sock_def_readable 中再一次訪問到了 sock->sk_wq 下的wait。回憶下我們前面調用 recvfrom 執行的最后,通過 DEFINE_WAIT(wait) 將當前進程關聯的等待隊列添加到 sock->sk_wq 下的 wait 里了。

那接下來就是調用 wake_up_interruptible_sync_poll 來喚醒在 socket 上因為等待數據而被阻塞掉的進程了。

  1. //file: include/linux/wait.h 
  2. #define wake_up_interruptible_sync_poll(x, m)    \ 
  3.  __wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, (void *) (m)) 
  1. //file: kernel/sched/core.c 
  2. void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode, 
  3.    int nr_exclusive, void *key
  4.  unsigned long flags; 
  5.  int wake_flags = WF_SYNC; 
  6.  
  7.  if (unlikely(!q)) 
  8.   return
  9.  
  10.  if (unlikely(!nr_exclusive)) 
  11.   wake_flags = 0; 
  12.  
  13.  spin_lock_irqsave(&q->lock, flags); 
  14.  __wake_up_common(q, mode, nr_exclusive, wake_flags, key); 
  15.  spin_unlock_irqrestore(&q->lock, flags); 

__wake_up_common 實現喚醒。這里注意下, 該函數調用是參數 nr_exclusive 傳入的是 1,這里指的是即使是有多個進程都阻塞在同一個 socket 上,也只喚醒 1 個進程。其作用是為了避免驚群。

  1. //file: kernel/sched/core.c 
  2. static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, 
  3.    int nr_exclusive, int wake_flags, void *key
  4.  wait_queue_t *curr, *next
  5.  
  6.  list_for_each_entry_safe(curr, next, &q->task_list, task_list) { 
  7.   unsigned flags = curr->flags; 
  8.  
  9.   if (curr->func(curr, mode, wake_flags, key) && 
  10.     (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) 
  11.    break; 
  12.  } 

在 __wake_up_common 中找出一個等待隊列項 curr,然后調用其 curr->func。回憶我們前面在 recv 函數執行的時候,使用 DEFINE_WAIT() 定義等待隊列項的細節,內核把 curr->func 設置成了 autoremove_wake_function。

  1. //file: include/linux/wait.h 
  2. #define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function) 
  3.  
  4. #define DEFINE_WAIT_FUNC(namefunction)    \ 
  5.  wait_queue_t name = {      \ 
  6.   .private = current,    \ 
  7.   .func  = function,    \ 
  8.   .task_list = LIST_HEAD_INIT((name).task_list), \ 
  9.  } 

在 autoremove_wake_function 中,調用了 default_wake_function。

  1. //file: kernel/sched/core.c 
  2. int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags, 
  3.      void *key
  4.  return try_to_wake_up(curr->private, mode, wake_flags); 

調用 try_to_wake_up 時傳入的 task_struct 是 curr->private。這個就是當時因為等待而被阻塞的進程項。當這個函數執行完的時候,在 socket 上等待而被阻塞的進程就被推入到可運行隊列里了,這又將是一次進程上下文切換的開銷。

小結

好了,我們把上面的流程總結一下。內核在通知網絡包的運行環境分兩部分:

第一部分是我們自己代碼所在的進程,我們調用的 socket() 函數會進入內核態創建必要內核對象。recv() 函數在進入內核態以后負責查看接收隊列,以及在沒有數據可處理的時候把當前進程阻塞掉,讓出 CPU。

第二部分是硬中斷、軟中斷上下文(系統進程 ksoftirqd)。在這些組件中,將包處理完后會放到 socket 的接收隊列中。然后再根據 socket 內核對象找到其等待隊列中正在因為等待而被阻塞掉的進程,然后把它喚醒。

 

每次一個進程專門為了等一個 socket 上的數據就得被從 CPU 上拿下來。然后再換上另一個進程。等到數據 ready 了,睡眠的進程又會被喚醒。總共兩次進程上下文切換開銷,根據之前的測試來看,每一次切換大約是 3-5 us(微秒)左右。如果是網絡 IO 密集型的應用的話,CPU 就不停地做進程切換這種無用功。

在服務端角色上,這種模式完全沒辦法使用。因為這種簡單模型里的 socket 和進程是一對一的。我們現在要在單臺機器上承載成千上萬,甚至十幾、上百萬的用戶連接請求。如果用上面的方式,那就得為每個用戶請求都創建一個進程。相信你在無論多原始的服務器網絡編程里,都沒見過有人這么干吧。

如果讓我給它起一個名字的話,它就叫單路不復用(飛哥自創名詞)。那么有沒有更高效的網絡 IO 模型呢?當然有,那就是你所熟知的 select、poll 和 epoll了。下次飛哥再開始拆解 epoll 的實現源碼,敬請期待!

這種模式在客戶端角色上,現在還存在使用的情形。因為你的進程可能確實得等 Mysql 的數據返回成功之后,才能渲染頁面返回給用戶,否則啥也干不了。

注意一下,我說的是角色,不是具體的機器。例如對于你的 php/java/golang 接口機,你接收用戶請求的時候,你是服務端角色。但當你再請求 redis 的時候,就變為客戶端角色了。

 

不過現在有一些封裝的很好的網絡框架例如 Sogou Workflow,Golang 的 net 包等在網絡客戶端角色上也早已摒棄了這種低效的模式!

 

責任編輯:武曉燕 來源: 開發內功修煉
相關推薦

2020-12-04 11:40:53

Linux

2025-01-13 13:00:00

Go網絡框架nbio

2009-07-07 18:08:03

刀片服務器刀片IDC

2013-09-10 10:04:05

云計算大數據NoSQL

2015-09-09 13:38:59

2015-04-14 10:34:02

微軟AzurePaaS云應用

2024-01-29 14:21:51

2012-05-24 10:53:19

云應用安全云計算

2009-03-03 12:48:01

2018-05-25 09:00:00

2022-04-24 10:42:59

Kubernete容器網絡Linux

2013-09-17 09:30:15

云項目實施云項目云計算技術

2022-05-05 15:13:59

數字化轉型組織文化組織

2010-08-25 09:07:03

2013-07-31 10:04:42

hadoopHadoop集群集群和網絡

2012-11-08 14:47:52

Hadoop集群

2012-08-31 10:00:12

Hadoop云計算群集網絡

2017-09-04 19:08:05

上線直播技術浪live

2020-06-17 16:43:40

網絡IO框架

2016-02-15 10:30:24

大數據大數據實施實施戰略
點贊
收藏

51CTO技術棧公眾號

成年网站在线视频网站| av中文字幕电影在线看| 99成人免费视频| 亚洲午夜久久久影院| 一级片视频免费观看| 国产欧美日本| 国产国语刺激对白av不卡| 久久99国产精品99久久| 在线播放蜜桃麻豆| 一区二区三区欧美在线观看| 成年人免费视频观看| 福利一区福利二区微拍刺激| 色哺乳xxxxhd奶水米仓惠香| 国产一区福利在线| 六月激情综合网| 午夜精品一区在线观看| 尤物网址在线观看| 欧美xfplay| 色婷婷综合久久久中字幕精品久久| 久久精品2019中文字幕| 欧美亚洲国产精品久久| 日韩国产高清一区| 久久久一区二区三区| 在线视频专区| 日韩精品中文字幕久久臀| 加勒比色综合久久久久久久久| 不卡的av一区| 久久久久久电影| freemovies性欧美| 久久精品国产2020观看福利| 一区久久精品| 国产videos| 欧美乱大交做爰xxxⅹ性3| av在线之家电影网站| 久久久久久久久久久黄色| 作爱视频免费观看视频在线播放激情网| 亚洲精品一区二区三区精华液| 国产一区二区欧美| 精品少妇一区二区三区在线| 91高清视频在线| 色综合中文网| 91专区在线观看| 欧美中文字幕不卡| 色天下一区二区三区| 日本aa在线观看| 亚洲国产精品久久91精品| 香港欧美日韩三级黄色一级电影网站| 久久av高潮av| 亚洲激情在线观看视频免费| 一区二区日本视频| 国产精品秘入口| 成人国产亚洲精品a区天堂华泰| a级高清视频欧美日韩| 欧美电影h版| 日本一欧美一欧美一亚洲视频| 91免费国产在线| 亚洲精品一区av| 粉嫩av一区二区三区天美传媒| 午夜伊人狠狠久久| 欧美交a欧美精品喷水| 日韩av电影免费| 国产精品午夜一区二区欲梦| 亚洲自拍另类综合| 欧美自拍一区| 明星乱亚洲合成图.com| 日韩中文字幕在线观看| 国产日韩欧美综合在线| 九九热播视频在线精品6| 九九99九九精彩| 欧美国产日韩精品| 国产精品乱码妇女bbbb| 香蕉人人精品| av网站在线免费观看| 一区二区三区精品国产| 日韩欧美在线一区| 亚洲精选成人| 国产激情久久| 香蒸焦蕉伊在线| 99久久久精品视频| 成人a在线视频| 一区二区三区视频免费在线观看| 一区二区高清在线| 日本欧美加勒比视频| 色网址在线观看| 日韩美女视频在线观看| 1区2区3区欧美| 一本色道久久亚洲综合精品蜜桃| 欧美成人性生活| 久久精品欧美一区二区三区麻豆| 国产精品日本一区二区不卡视频 | 欧美超级乱淫片喷水| 国产成人综合在线播放| 色黄视频在线观看| www.四虎成人| 欧美一区二三区| 亚洲精品成人在线| 女人香蕉久久**毛片精品| 日韩毛片久久久| 欧美亚洲免费高清在线观看| 欧美一级国产精品| 久久伊99综合婷婷久久伊| 日本成人a网站| 精彩国产在线| 青草全福视在线| 国产精品国模在线| 日韩欧美综合在线| 国产91丝袜在线观看| 久久精品亚洲成在人线av网址| 美女被人操视频在线观看| 国产一区二区精品免费| 亚洲午夜精品视频| 亚洲黄色免费电影| 国产一区二三区好的| 99久久99视频只有精品| 欧美日韩卡一| 九色视频在线播放| 日韩国产欧美亚洲| 国产亚洲一区在线播放| 夜夜躁日日躁狠狠久久88av| 色婷婷亚洲一区二区三区| jlzzjlzz亚洲日本少妇| 欧美久久99| 日韩极品在线| 国产欧美自拍| 91三级在线| 亚洲精品少妇久久久久久| 青青草视频国产| 欧洲精品一区色| 国产在线视频不卡| 欧美黄色成人网| 中文国产成人精品久久一| 欧美日韩精品一区二区天天拍小说| 2022国产精品视频| 成人av网址在线观看| 国产精品亚洲第一区在线暖暖韩国| 亚洲第一区色| 99久久www免费| 欧美在线电影| 亚洲裸色大胆大尺寸艺术写真| 视频欧美一区| 亚洲天堂av资源在线观看| 国产人妖一区| 911亚洲精品| 乱亲女h秽乱长久久久| 国产欧美日韩精品高清二区综合区| 亚欧洲精品视频在线观看| www国产在线观看| 亚洲成a人片| 在线综合色站| 欧美午夜精彩| 性一交一乱一区二区洋洋av| 久久国产毛片| 99免费精品在线观看| 亚洲久本草在线中文字幕| 一本色道综合亚洲| 亚洲第一视频网| www.xxxx精品| 热久久这里只有精品| 五月开心婷婷久久| 精品国产乱码久久久久久老虎| 国产视频精品久久久| 欧美成人午夜激情| 亚洲xxxxx性| 国产一区二区三区在线免费| 激情婷婷综合网| 国产最新视频在线观看| 丁香花高清在线观看完整版| 国产精品igao视频网网址不卡日韩 | 国产亚洲成人一区| 久久天天综合| 国产日韩精品视频一区| 在线观看网站黄不卡| 色婷婷综合久久久久| 成人中文字幕在线观看| 国产精品h视频| 369你懂的电影天堂| 日韩另类在线| 欧美精品国产一区| 亚洲国产激情av| 日韩h在线观看| 国产精品久久久久久久久久小说| 日本免费黄色小视频| yes4444视频在线观看| 五月天亚洲色图| 国产乱子伦视频一区二区三区| 亚洲午夜精品一区二区三区他趣| www日韩中文字幕在线看| 欧美精品123| 五丁香在线视频| 日韩精品一页| 国产精品456| 日韩精品一区二区三区四区| 国产精品美女久久久久久免费| 国产又黄又猛视频| 丁香影院在线| 日本va欧美va精品发布| 欧美久久久久久蜜桃| 91青青草免费在线看| 中文字幕电影在线| 欧美一区二区三区久久精品茉莉花 |