精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Linux 內核源碼分析之進程概要及調度時機

系統 Linux
人們在面對一個問題束手無策的時候,經常會創造一個概念,然后基于這個概念來演化出一個系統來解決這個問題。進程的概念就是人類發明出來,為了解決物理世界想要同時做若干件事情的需求,最終演化出了進程子系統。

[[440610]]

 本文涉及的 Linux 內核版本是 v5.0,可在 「https://elixir.bootlin.com/linux/v5.0/source」 在線瀏覽,文中每段源碼均標注了文件路徑及行數,建議讀者對著源碼讀此文。

進程概要及調度時機

這篇文章從 Linux 內核層面分析進程概要及調度時機。

0 本文內容一分鐘速覽

如果讀者沒有耐心看完整篇文章,下面是本文的核心內容預覽,一分鐘內能讀完。

0.1 進程概要

  • 進程是對物理世界的建模抽象,每個進程對應一個 task_struct 數據結構,這個數據結構包含了進程的所有的信息。
  • 在 Linux 內核中,不會區分線程和進程的概念,線程也是通過進程來實現的,線程和進程的唯一區別就是:線程沒有獨立的資源,進程有。
  • 所有的進程都是通過其他進程創建出來的,因此,整個進程組織為一棵進程樹。
  • 0 號進程是 無中生有 憑空產生的,是靜態定義出來的,是所有進程的祖先,創建了 INIT(1號)和 kthreadd(2號進程)。

0.2 進程調度時機

  • 系統調用 yield、pause 會使得當前進程讓出 CPU,隨后進行一次進程調度。
  • 系統調用 futex(wait) 等待某個信號量,將進程設置為 TASK_INTERRUPTIBLE 狀態,然后進行一次進程調度。
  • 進程在退出的時候,會系統調用到 exit 方法,將當前進程設置為 TASK_DEAD 之后,進行一次進程調度。
  • 在創建新進程、喚醒進程、周期調度過程中,內核會給當前進程設置一個需要調度的標志,然后在下一次中斷返回到用戶空間時,進行一次調度。
  • 每顆 CPU 都會綁定一個 IDLE 進程,沒事就在 CPU 上無聊地空轉,偶爾進行一次進程調度。

1 進程概要

1.1 進程是對物理世界的建模抽象

人們在面對一個問題束手無策的時候,經常會創造一個概念,然后基于這個概念來演化出一個系統來解決這個問題。進程的概念就是人類發明出來,為了解決物理世界想要同時做若干件事情的需求,最終演化出了進程子系統。關于進程的基本知識網上有很多,這里說下我的理解:

  • 加載器將可執行程序文件(Linux 中是 ELF 格式)加載到操作系統,操作系統中就多了一個進程。
  • 進程的核心由代碼段和數據段組成,代碼段就是進程在執行過程中按照正常流程一條條執行的指令,數據段就是指令需要的數據。
  • 每顆 CPU 都有一個 PC(Program Counter)寄存器,這個寄存器指向了下一條要執行的指令地址,由于這個指令必然屬于某個進程,所以,每個 CPU 每一時刻只能運行一個進程。
  • 多線程在內核空間本質上也是多進程,多個進程在時間較大的尺度上給人一種可以同時執行的錯覺,本質上是通過進程調度交叉執行,只不過這個時間太短,我們感覺不到而已。
  • JVM 中的一個線程對應了 Linux 內核中的一個進程,了解了底層進程的機制,也就了解了上層的很多現象。

1.2 進程的數據結構

由于歷史原因,內核中表示進程的數據結構叫做 task_struct,這個數據結構里面的字段有幾十個,我不太想一一列出來,然后占很大篇幅。我會列幾個大家比較關心的,在后面的分析過程中,會逐漸展開 task_struct 的其他字段。本篇文檔對應的 Linux 內核是 5.0。

  1. // include/linux/sched.h:592 
  2. // Linnux 進程底層對應的數據結構 
  3. struct task_struct { 
  4. //  進程的 ID 
  5.     pid_t pid; 
  6. //  進程的狀態     
  7.     volatile long state; 
  8. //  進程的父親     
  9.     struct task_struct *parent; 
  10. //  當前進程的子進程     
  11.     struct list_head children;   
  12. }; 

從上面的幾個關鍵的字段可以看出,每個進程都有唯一的 ID 和狀態,并且,在系統中,進程是通過一棵樹的方式來組織的,也就是說,所有的進程都有父親,通過我們熟悉的 fork 系統調用來創造。另外,Linux 內核中也是不區分進程和線程的,兩者均使用 task_struct 數據結構,線程的本質是共享進程的資源,對應這個數據結構,只要把里面涉及共享的指針指向進程的資源即可。

1.3 特殊的進程

「所有的進程都有父親」,這句話不一定全對,就像演繹邏輯鏈一樣,我們一直順著大前提往上追,總會追到第一個 大 bug,這個 大 bug 我們無法證明,只能默認它是對的,它是我們系統的第一性原理。扯遠了,Linux 中,這個 大 bug 就是 0 號進程,它的另一個外號叫 IDLE,這個 大 bug 在內核初始化的時候,被顯示地定義出來(而不是通過 fork),下面我們來感受一下 Linux 進程子系統中第一個進程 無中生有 的過程。

  1. // include/linux/sched/task.h:26 
  2. extern struct task_struct init_task; // 這個就是 0 號進程 
  3.  
  4. // init/init_task.c:57 
  5. struct task_struct init_task = { 
  6. //  這個字段沒有顯示定義出來,而是通過 struct pid 來描述,效果一樣         
  7.     .pid = 0, 
  8. //  對應了 TASK_RUNNING     
  9.     .state = 0, 
  10. //  我就是第一個進程,我沒有 parent     
  11.     .parent = &init_task, 
  12. //  初始化子進程鏈表     
  13.     .children = LIST_HEAD_INIT(init_task.children), 
  14. }; 

init_task 類似于盤古,系統中所有的進程都是由它開辟出來的,在后續的 Linux 內核文章中,我們會逐漸了解這個機制的妙處,我們先把注意力調回到本篇文章的重點,進程切換的機制。

1.4 進程概要小結

  • 進程是對物理世界的建模抽象,每個進程對應一個 task_struct 數據結構,這個數據結構包含了進程的所有的信息。
  • 在 Linux 內核中,不會區分線程和進程的概念,線程也是通過進程來實現的,線程和進程的唯一區別就是:線程沒有獨立的資源,進程有。
  • 所有的進程都是通過其他進程創建出來的,因此,整個進程組織為一棵進程樹。
  • 0 號進程是 無中生有 憑空產生的,是靜態定義出來的,是所有進程的祖先。

2 進程調度時機

Linux 內核中,進程調度的時機無處不在,我們來了解幾個典型的時機。

2.1 yield 和 pause 讓出 cpu

通常情況下,我們的進程運行在用戶空間,通過系統調用進入到內核空間,從而做一些更高級的事情。

yield 系統調用可以讓當前進程放棄 cpu,進行系統的調度。

  1. // kernel/sched/core.c:4963 
  2. SYSCALL_DEFINE0(sched_yield) { 
  3.     do_sched_yield(); 
  4.     return 0; 

Linux 中的系統調用通過類似 SYSCALL_DEFINEx 這種方式定義,x 表示參數的個數,sched_yield 系統調用沒有參數,所以 x 是 0。

我們沿著調用鏈往下,來到 do_sched_yield 方法。

  1. //  kernel/sched/core.c:4942 
  2. static void do_sched_yield(void) {    
  3.     ... 
  4.     schedule(); // :4960 
  5.     ... 

我們發現,在 4960 行,有一個命名非常簡單的函數調用,叫做 schedule(),這個函數就是內核中進程調度的入口,我們分析進程調度的時機,等價于查看有哪些地方調用了這個方法。

下面我們來看看 pause 這個系統調用:

  1. // kernel/signal.c:4170 
  2. SYSCALL_DEFINE0(pause) {    
  3.     __set_current_state(TASK_INTERRUPTIBLE); 
  4.     schedule(); 
  5.  
  6. // include/linux/sched.h:185 
  7. #define __set_current_state(state_value) \ 
  8.  current->state = (state_value) 

pause 系統調用首先將當前進程設置為 TASK_INTERRUPTIBLE 狀態,其實就是給 task_struct 結構中的 state 字段賦值,附上 TASK_INTERRUPTIBLE 之后,在后續進程調度中就可以過濾掉這個進程,選擇其他的進程進行調度。接著,同樣是一個簡單的 schedule 函數,進入到調度的邏輯。

2.2 futex 等待資源

futex (fast userspace mutex),用來給上層應用構建更高級別的同步機制,是實現信號量和鎖的基礎,后面有機會可以單獨介紹。我們簡化一下場景:一個進程在等待某個信號的時候,最終會通過系統調用進入到 futex,其中某個關鍵參數為 wait:

  1. // kernel/futex.c:3633 
  2. SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val, 
  3. struct __kernel_timespec __user *, utime, u32 __user *, uaddr2, 
  4. u32, val3) { 
  5.     ... 
  6.     return do_futex(... op, ...); // :3665 

這個系統調用有 6 個參數,參數類型和名稱并列展開,上層應用在等待一個信號量的時候,給 op 這個參數的傳遞的是 FUTEX_WAIT_BITSET,我們通過調用鏈往下追。

  1. // kernel/futex.c:3573 
  2. long do_futex(...int op,...) { 
  3.     int cmd = op & FUTEX_CMD_MASK; 
  4.  
  5.     switch (cmd) { 
  6.         case FUTEX_WAIT_BITSET: 
  7.             return futex_wait(uaddr, flags, val, timeout, val3); // :3604 
  8.     ... 
  9.     } 
  10.     ... 

由于中間調用鏈有點長,下面我們就簡化一下調用邏輯,專注核心,這個在我們去閱讀源碼過程中,也是非常重要的一點,閱讀核心邏輯的時候,不要被太多的細節干擾。

  1. // kernel/futex.c:2679 
  2. static int futex_wait(...) { 
  3.     ... 
  4.     futex_wait_queue_me(...); // :2713 
  5.     ... 
  6.  
  7. // kernel/futex.c:2571  
  8. static void futex_wait_queue_me(...) { 
  9.     ... 
  10. //  這里可以看到,調用 futex 的進程將變為睡眠狀態,與我們的認知一致 
  11.     set_current_state(TASK_INTERRUPTIBLE); // :2580 
  12.     ... 
  13.     freezable_schedule(); // :2598 
  14.     ... 
  15.  
  16. // include/linux/freezer.h:169 
  17. static inline void freezable_schedule(void) { 
  18.     ... 
  19.     schedule(); // :180 
  20.     ... 

沿著進程調用鏈下來,我們可以看到,進程系統調用 futex(wait) 時,可能會將自己設置為睡眠狀態并且進行一次進程調度。

2.3 exit 進程退出

多年的編程經驗告訴我們,在一個進程退出的時候會觸發進程調度,我們通過內核源碼來證明這一點。應用層的進程在退出時,最終會通過 exit 系統調用進入到內核,調用鏈如下:

  1. // kernel/exit.c:946 
  2. SYSCALL_DEFINE1(exit, int, error_code) { 
  3.     do_exit((error_code&0xff)<<8); 
  4.  
  5. // kernel/exit.c:773 
  6. void do_exit(long code) { 
  7.     ... 
  8.     do_task_dead(); // :933 
  9.  
  10. // kernel/sched/core.c:3494 
  11. void do_task_dead(void) { 
  12. // 這個地方也是給 task_struct 中的 state 字段賦值 
  13.     set_special_state(TASK_DEAD); 
  14.     ... 
  15.     __schedule(false); // :3502 
  16.     ... 

通過調用鏈,我們可以看到,進程在退出的時候,最終調用了 __schedule 方法,這里我們可以將這個方法等價于 schedule 方法,因為 schedule 方法最終會調用到這個方法,__schedule 中描述了進程調度的核心邏輯。

2.4 中斷返回時調度

除了上述調度時機,還有一類調度時機是中斷返回的時候。

介紹中斷之前,先描述一下什么是異常:進程的指令按照程序正常流程一直在 CPU 上跑,系統突然發生了一個帶有異常號的異常,強迫 CPU 停止執行當前的指令,CPU 隨后會在執行完當前指令之后,保存現場,根據異常號跳轉到異常處理程序,處理完之后,回到被異常終止的下一條機器指令繼續執行。

系統調用是常見一種類型的異常,也是應用代碼從用戶空間主動進入內核空間的唯一方式。另外一種常見的異常就是硬件中斷,比如我們點下鼠標、按下鍵盤、網卡接收到數據、磁盤數據讀寫完畢等,都會觸發一次硬件中斷,運行在用戶空間的進程會被動陷入到內核空間,進行中斷處理程序的處理。

而中斷處理程序處理完之后,勢必要返回到用戶空間,在返回至用戶空間之前,會順帶做一件事情,判斷是否要進行進程調度,如果需要,則順帶做一次進程調度。我們通過調用鏈來分析一下這個過程。

我們拿 arm64 處理器為例,中斷處理程序的的入口是 el0_irq,這里看不懂匯編沒有關系,我們抓關鍵部分即可。

  1. // arch/arm64/kernel/entry.S:838 
  2. // 這里即是 arm64 的中斷入口 
  3. el0_irq: 
  4.     ... 
  5.     處理中斷 
  6.     ... 
  7. //  回到用戶空間 
  8.     b ret_to_user // :834 
  9.      
  10. // arch/arm64/kernel/entry.S:895 
  11. ret_to_user: 
  12.     ... 
  13.     ldr x1, [tsk, #TSK_TI_FLAGS] // :890 
  14.     and x2, x1, #_TIF_WORK_MASK 
  15.     cbnz x2, work_pending 

890 行代碼想要表述的是,將 tsk(也就是被中斷暫停的當前進程)數據結構中,偏移量為 TSK_TI_FLAGS 傳遞給 x1 寄存器,順帶說一下,arm64 中有 x0 ~ x31 寄存器。

TSK_TI_FLAGS 常量在 asm-offsets.c 文件中被定義。

  1. // arch/arm64/kernel/asm-offsets.c:48 
  2. int main(void) { 
  3.     ... 
  4.     DEFINE(TSK_TI_FLAGS, offsetof(struct task_struct, thread_info.flags)) // :442 
  5.     ...         

本質上,就是 task_struct 結構中的 thread_info 結構中的 flags 字段的偏移量:

  1. // include/linux/sched.h:592 
  2. struct task_struct { 
  3.     ... 
  4.     struct thread_info thread_info; // :598 
  5.     ... 
  6.  
  7. // arch/arm64/include/asm/thread_info.h:39 
  8. struct thread_info { 
  9.     ... 
  10.     unsigned long flags; // :40 
  11.     ... 

所以 ret_to_user 中的這個邏輯就是,取出 task_struct->thread_info->flags 字段,然后通過與 _TIF_WORK_MASK 進行 and 操作:

  1. // arch/arm64/include/asm/thread_info.h:118 
  2. #define _TIF_WORK_MASK  (_TIF_NEED_RESCHED | _TIF_SIGPENDING | \ 
  3.      _TIF_NOTIFY_RESUME | _TIF_FOREIGN_FPSTATE | \ 
  4.      _TIF_UPROBE | _TIF_FSCHECK) 

進程中的 flags 與 _TIF_WORK_MASK 進行 and 操作之后,如果二進制位的值不為 0,就跳轉(cbnz)到 work_pending 方法。

  1. // arch/arm64/kernel/entry.S:884  
  2. work_pending: 
  3.     ... 
  4.     bl  do_notify_resume // :886 
  5.     ... 
  6.      
  7. // arch/arm64/kernel/signal.c:915    
  8. // 參數中 thread_flags 的值就是上面保存在 x1 寄存器中的值,也就是 `task_struct->thread_info->flags` 
  9. void do_notify_resume(... long thread_flags) { 
  10.     ... 
  11.     if (thread_flags & _TIF_NEED_RESCHED) { 
  12.         schedule(); // :933 
  13.     }  
  14.     ... 

到了這里,中斷返回到用戶空間的調度邏輯大家應該比較清楚了。我們總結一點就是:當中斷處理程序返回用戶空間的時候,如果被中斷的進程被設置了需要進程調度標志,那么就進行一次進程調度。

那么,什么時候當前進程會被設置這個標志?

只有進入到內核空間才能夠設置當前進程的需要調度標志,而系統調用是我們主動從用戶空間進入內核空間的唯一方式,下面我們就來分析有哪些系統調用會設置當前進程需要調度的標志。

2.4.1 創建新進程

第一類是是通過 fork 系統調用創建新的進程。相信大家應該或多或少聽過,大多數編程語言創建線程,比如 Java 的 new Thread(...).start(),最后都會落到 fork 系統調用。

接下來,我們來分析 fork 系統調用是如何來設置進程需要調度的標識的。

  1. // kernel/fork.c:2291 
  2. SYSCALL_DEFINE0(fork) { 
  3.     ... 
  4.     return _do_fork(...); 
  5.  
  6. // kernel/fork.c:2196 
  7. long _do_fork(...) { 
  8.     struct task_struct *p; 
  9.     ... 
  10. //  大多數數據結構都是 copy 的父進程,也就是當前進程 
  11.     p = copy_process(...); // :2227 
  12.     ... 
  13. //  創建完子進程之后,讓子進程 "蘇醒" 
  14.     wake_up_new_task(p); // :2252 
  15.     ... 

這里我們可以看到,創建子進程的時候,有部分工作是復制父進程(2227 行),也就是當前進程的數據結構,線程和進程的本質區別就在這個方法里面,用一個參數確定要復制哪些資源,我們在后面的文章中會詳細分析進程創建過程,這里我們點到為止。

創建完新進程之后,調用 wake_up_new_task 喚醒新進程,我們來看內核是如何喚醒新進程的。

  1. // kernel/sched/core.c:2413 
  2. void wake_up_new_task(struct task_struct *p) { 
  3.     ... 
  4. //  將當前進程設置為 RUNNING 狀態,后續即可調度 
  5.     p->state = TASK_RUNNING; // :2419  
  6.     ... 
  7. //  判斷是否要搶占當前進程 
  8.     check_preempt_curr(rq, p, WF_FORK); // :2439 
  9.     ... 

check_preempt_curr 會根據當前進程的調度類型,執行對應的方法:

  1. // kernel/sched/core.c:854 
  2. void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags) { 
  3.     ... 
  4. //  rq 是當前 cpu 上的進程隊列 
  5. //  curr 是當前正在 cpu 運行的進程 
  6. //  sched_class 是當前進程的調度 
  7.     rq->curr->sched_class->check_preempt_curr(rq, p, flags); // :858 
  8.     ... 

sched_class 表示進程的調度類型,這個字段在每個 task_struct 中。

  1. // include/linux/sched.h:592 
  2. struct task_struct { 
  3.     ... 
  4. //  sched_class 在進程的數據結構中 
  5. //  表示調度類型,我們后面的系列文章再詳細分析  
  6.     const struct sched_class *sched_class; // :643 
  7.     ... 
  8.  
  9. // kernel/sched/sched.h:1715 
  10. // Linux 中所有的調度類型 
  11. extern const struct sched_class stop_sched_class; 
  12. extern const struct sched_class dl_sched_class; 
  13. extern const struct sched_class rt_sched_class; 
  14. extern const struct sched_class fair_sched_class; 
  15. extern const struct sched_class idle_sched_class; 

可以看到,Linux 中一共有五種調度類型,fair_sched_class 是一般進程的調度類型,稱為公平調度,我們后面的文章中再詳細分析這五個調度類型,這里,我們還是聚焦重點。

我們跟隨調用鏈,來到 fair_sched_class 的 check_preempt_check 方法。

  1. // kernel/sched/fair.c:10506 
  2. const struct sched_class fair_sched_class = { 
  3.     .check_preempt_curr = check_preempt_wakeup // :10513 
  4.  
  5. // kernel/sched/fair.c:6814 
  6. static void check_preempt_wakeup(rq *rq, task_struct *p...) { 
  7.     struct task_struct *curr = rq->curr; 
  8.     struct sched_entity *se = &curr->se, *pse = &p->se; 
  9.   
  10. //  如果 pse 的虛擬時間小于當前進程的虛擬時間,就搶占 
  11.     if (wakeup_preempt_entity(se, pse) == 1) { // :6867 
  12.         goto preempt; 
  13.      } 
  14. preempt: // :6879 
  15. //  沒有在這里直接調度,而是設置了一個標志,在異常處理返回的時候統一調度 
  16.     resched_curr(rq); 

check_preempt_wakeup 方法中一處關鍵的地方,se 表示當前進程的調度實體,pse 表示 fork 出來的進程的調度實體。

調度實體這個對象也定義在進程的數據結構中。

  1. // include/linux/sched.h:592 
  2. struct task_struct { 
  3.     ... 
  4.     struct sched_entity se; // :644 
  5.     ... 

調度實體是為了防止一個進程不斷地 fork 多個子進程,從而無限霸占 cpu,內核可以將一組線程綁定到一起進行統一調度,這里我們不用關心太多細節,仍然聚焦核心。

下面我們來看下 check_preempt_wakeup 方法中 6867 行的 wakeup_preempt_entity 代碼做了什么事情。

  1. // kernel/sched/fair.c:6767 
  2. static int wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se) { 
  3.     s64 gran, vdiff = curr->vruntime - se->vruntime; 
  4.  
  5.     if (vdiff <= 0) 
  6.         return -1; 
  7.      
  8. //  gran 可以理解為進程運行的最小時間片 
  9.     gran = wakeup_gran(se);  
  10.     if (vdiff > gran) 
  11.         return 1; 
  12.  
  13.     return 0; 

公平調度類默認會通過進程的優先級和歷史運行情況來計算出一個進程運行的虛擬時間,虛擬時間小的進程可以搶占虛擬時間大的進程。

當然,為了防止頻繁搶占調度,要保證進程在 cpu 上的一個最小的運行時間,這個時間默認在 v5.0 內核中是 100 毫秒。

上面這段代碼的邏輯,總結來說就是,如果當前進程的時間片已到,并且當前進程的虛擬時間小于 fork 出來的進程的虛擬時間片(顯然是 0),則返回 1,然后進入到標記為 preempt 的代碼,即 resched_curr。

  1. // kernel/sched/core.c:465 
  2. void resched_curr(struct rq *rq) { 
  3.     ... 
  4.     set_tsk_need_resched(curr); // :483 
  5.     ... 
  6. }     
  7.  
  8. // include/linux/sched.h:1676 
  9. static inline void set_tsk_need_resched(struct task_struct *tsk) { 
  10.     set_tsk_thread_flag(tsk,TIF_NEED_RESCHED); 

resched_curr 給當前進程設置一個標志,需要進行一次調度,根據我們上一節的分析,下一次中斷返回到用戶空間的時候,就會進行一次調度。

2.4.2 futex 喚醒進程

除了 fork 系統調用,在 futex 系統調用的時候,也會設置需要調度的標志。

  1. // kernel/futex.c:3633 
  2. SYSCALL_DEFINE6(futex, ... op, ...) { 
  3.     ... 
  4.     return do_futex(... op, ...); // :3665 

這種情況下,用戶傳遞的 op 參數是 FUTEX_WAKE_OP,即用戶需要進行喚醒操作,我們通過調用鏈往下追:

  1. // kernel/futex.c:3573 
  2. long do_futex(...int op,...) { 
  3.  int cmd = op & FUTEX_CMD_MASK; 
  4.  
  5.  switch (cmd) { 
  6.         case FUTEX_WAKE_OP: 
  7.             return futex_wake_op(...); // :3615 
  8.     ... 
  9.     } 
  10.     ... 
  11.  
  12. // kernel/futex.c:1683 
  13. static int futex_wake_op(...) { 
  14.     ... 
  15.     wake_up_q(...); // :1766 
  16.     ... 
  17.  
  18. // kernel/sched/core.c:436 
  19. void wake_up_q(...) { 
  20.     wake_up_process(task); // :453 
  21.  
  22. // 后續調用鏈路有些長,我們中間的代碼描述簡化處理,最終會落到下面的代碼 
  23.  
  24. // kernel/sched/core.c:1667 
  25. static void ttwu_do_wakeup(...) { 
  26.     check_preempt_curr(...); 

可以看到,futex 的 wake 操作,最后同樣會落到和 fork 一樣的方法 check_preempt_curr,這個方法我們上面剛分析過,做的事情就是給當前線程設置一個需要調度的標志,在下一次中斷返回時進行一次調度。

2.4.3 周期調度

除了系統調用,內核還有一個定時調度機制:周期調度,內核會周期地調用 scheduler_tick 方法執行調度邏輯,我們來分析一下這個過程。

  1. // kernel/sched/core.c:3049 
  2. /* 
  3.  * This function gets called by the timer code, with HZ frequency. 
  4.  */ 
  5. void scheduler_tick(void) { 
  6.     ... 
  7. //  當前是哪個 cpu? 
  8.     int cpu = smp_processor_id(); 
  9. //  拿到 cpu 上的進程隊列 
  10.     struct rq *rq = cpu_rq(cpu); 
  11. //  拿到 cpu 上當前運行的進程 
  12.     struct task_struct *curr = rq->curr; 
  13.     ... 
  14.     curr->sched_class->task_tick(rq, curr, 0); // :3061 
  15.     ... 

scheduler_tick 調用當前進程的調度類的 task_tick 方法,我們還是分析常見的公平調度類的 task_tick 方法。

  1. // kernel/sched/fair.c:10506  
  2. const struct sched_class fair_sched_class = { 
  3.     ...     
  4.     .task_tick = task_tick_fair, // :10530 
  5.     ... 
  6.  
  7. // kernel/sched/fair.c:10030 
  8. static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued) { 
  9.  struct cfs_rq *cfs_rq; 
  10.  struct sched_entity *se = &curr->se; 
  11.     ... 
  12. //  cfs_rq 可以理解為當前 cpu 上公平調度類的進程隊列 
  13.     cfs_rq = cfs_rq_of(se); 
  14.     entity_tick(cfs_rq, se, queued); // :10037 
  15.     ... 
  16.  
  17. // kernel/sched/fair.c:4179 
  18. static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued) { 
  19. //  更新當前進程的運行時間 
  20.     update_curr(cfs_q); 
  21.     ... 
  22. //  更新當前進程的 load 
  23.     update_load_avg(cfs_rq, curr, UPDATE_TG); 
  24.     ... 
  25. //  如果 cpu 有就緒進程 
  26.     if (cfs_rq->nr_running > 1) 
  27.         check_preempt_tick(cfs_rq, curr); 

cfs_rq->nr_running 可以理解為當前 cpu 上,公平調度類型的就緒進程和運行進程的和,大于 1 表示有待調度的就緒進程,于是調用 check_preempt_tick:

  1. // kernel/sched/fair.c:4023 
  2. static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) { 
  3.     unsigned long ideal_runtime, delta_exec; 
  4.     struct sched_entity *se; 
  5.     ... 
  6.     ideal_runtime = sched_slice(cfs_rq, curr); 
  7.     delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; 
  8.     if (delta_exec > ideal_runtime) { 
  9.         resched_curr(rq_of(cfs_rq)); // :4056 
  10.     } 
  11.     ... 

check_preempt_tick 方法中,會計算一個進程的理想運行時間,理想運行時間是調度周期 * 當前調度實體權重 / 所有實體權重,如果當前進程運行的時間超過了這個理想運行時間,就嘗試一次調度,即調用 resched_curr,這個方法我們在上面分析過:給當前進程設置一個需要調度的標志,這樣在下一次中斷處理返回時,就會進行一次調度。

2.4.4 中斷處理返回時調度小結

  • 異常的本質就是程序不按照正常的流程走。系統調用是一種異常,硬件中斷也是一種異常,比如我們點擊了鼠標,按下了鍵盤,都觸發了一次異常。
  • 內核在處理中斷處理返回到用戶空間時,會判斷當前進程是否有設置需要調度的標志,如果有,就進行一次進程調度。
  • 某些系統調用,如 fork、futex 會在系統調用處理邏輯中設置需要調度的標記,這樣在下一次中斷返回就可以進行調度。
  • 除了系統調度,內核會周期性地給內核設置需要調度的標記,一旦當前進程總運行時間超了,就設置這個標記,下一次中斷返回就可以進行調度。

2.5 IDLE 進程調度

本文開篇提到了操作系統中的第一個進程,0 號進程,內核 無中生有 地創建完這個進程,這個進程總得干點啥。

其中一件事情就是不斷進行進程調度,我們來分析一下這個過程。

2.5.1 第一顆 CPU 上的 IDLE 進程

內核在啟動過程中,第一顆 CPU 進入到 start_kernel 方法,這個方法可以看做初始化整個內核的入口,在調用這個方法之前,0 號進程已經靜態地綁在了當前的 CPU 上,參考本文 1.3 小節。

  1. // init/main.c:537 
  2. // 在第一顆 CPU 上執行,當前進程的是 0 號進程 
  3. void start_kernel(void) { 
  4.    ... 
  5. // 一系列初始化操作 
  6.    ... 
  7.    arch_call_rest_init(); // :739 

關于內核的初始化,我們后面再分析,這里我們還是聚焦于 0 號進程的調度邏輯。

  1. // init/main.c:532 
  2. void arch_call_rest_init(void) { 
  3.  rest_init(); // :534 
  4.  
  5. // init/main.c:397 
  6. void rest_init(void) { 
  7.  int pid; 
  8.     ... 
  9. //  0 號進程創建了 1 號進程 init 
  10.     pid = kernel_thread(kernel_init,...); // :408 
  11.     ... 
  12. //  0 號進程創建了 2 號進程 kthreadd 
  13.     pid = kernel_thread(kthreadd,...); // :420  
  14.     ... 
  15. //  調度邏輯 
  16.     cpu_startup_entry(CPUHP_ONLINE); 

0 號進程創建了 1 號進程和 2 號進程,我們通過 ps -ef 指令是可以看到這兩個進程,如下圖所示。

1 號進程和 2 號進程

其中的 PPID 就是指的父進程的進程 ID。用戶空間的所有的進程的祖先都是 1 號進程,讀者可以在自己的 Linux 系統上使用 ps -ef 驗證這一點。

關乎這兩個頂級進程的詳細分析,我們后面的文章會提到,這里我們還是聚焦于 0 號進程的調度邏輯。

0 號進程創建了兩個頂級進程之后,調用 cpu_startup_entry

  1. // kernel/sched/idle.c:348 
  2. void cpu_startup_entry(...) { 
  3.     while (1) 
  4.         do_idle(); 
  5. // kernel/sched/idle.c:224 
  6. static void do_idle(void) { 
  7.     ... 
  8.     schedule_idle(); // :286 
  9.     ... 
  10.  
  11. // kernel/sched/core.c:3545 
  12. void schedule_idle(void) { 
  13.     ... 
  14.     __schedule(false); // :3556 
  15.     ... 

從上面的調用鏈可以看到,0 號進程會用一個 while 死循環,不斷反復地做一件事情,這個事情就是調度。

0 號進程可以理解為系統中所有進程中優先級最低的進程,當沒有進程可選中被調度,就選擇 0 號進程,而 0 號進程所做的事情就是一個死循環邏輯,由此可見,這個進程確實閑得慌,所以也叫做 IDLE 進程,后面我們統稱為 IDLE 進程。

2.5.2 其余 CPU 上的 IDLE 進程

除了第一顆 CPU 上有個 IDLE 進程不斷在跑,其余 CPU 也都有 IDLE 進程不斷在跑,這些個進程是第一顆 CPU 上的 IDLE 進程創建出來的,我們來分析一下這個過程。

在上面的 rest_init 方法中,第一顆 CPU 上的 IDLE 進程調用 kernel_thread 創建了 1 號進程,它的入口函數是 kernel_init,所以也叫 INIT 進程。

下面,我們來追一下這個調用鏈。

  1. // init/main.c:1050 
  2. static int kernel_init(void *unused) { 
  3.     ... 
  4.     kernel_init_freeable(); // :1054 
  5.     ... 
  6.  
  7. // init/main.c:1103 
  8. static void kernel_init_freeable(void) { 
  9.     ... 
  10.     smp_init(); // :1129 
  11.     ... 
  12.  
  13. // kernel/smp.c:563 
  14. void smp_init(void) { 
  15.     ... 
  16. //  創建出其他的 IDLE 進程  
  17.     idle_threads_init();  
  18.     pr_info("Bringing up secondary CPUs ...\n"); 
  19.     ... 
  20. //  啟動其他 CPU 
  21.     for_each_present_cpu(cpu) { 
  22.     ... 
  23.         cpu_up(cpu); 
  24.     } 

在 smp_init 方法中,先通過 idle_threads_init 方法復制出一堆 IDLE 進程,假設有 4 顆 CPU,除去當前進程,就復制出 3 個 IDLE 進程。

  1. // kernel/smpboot.c:66 
  2. void idle_threads_init(void) { 
  3.     unsigned int cpu, boot_cpu; 
  4.  
  5.     boot_cpu = smp_processor_id(); 
  6.  
  7.     for_each_possible_cpu(cpu) { 
  8.        if (cpu != boot_cpu) 
  9.            idle_init(cpu); 
  10.      } 
  11.  
  12. // kernel/smpboot.c:50 
  13. static void idle_init(unsigned int cpu) { 
  14.     struct task_struct *tsk = per_cpu(idle_threads, cpu); 
  15.  
  16.     if (!tsk) { 
  17. //      復制進程  
  18.         tsk = fork_idle(cpu); 
  19.         per_cpu(idle_threads, cpu) = tsk; 
  20.     } 

上面的邏輯即是,如果某個 CPU 上沒有綁定 IDLE 進程,就調用 fork_idle 進行創建,通過 per_cpu 進行綁定。

這些IDLE 進程初始化完成之后,開始加載其余 CPU,入口函數是 secondary_start_kernel,我們還是拿 arm64 架構為例來分析。

  1. // arch/arm64/kernel/smp.c:187 
  2. void secondary_start_kernel(void) { 
  3.     ... 
  4.     cpu_startup_entry(CPUHP_AP_ONLINE_IDLE); // :252  
  5.  
  6. // kernel/sched/idle.c:348 
  7. void cpu_startup_entry(...) { 
  8.     while (1) 
  9.         do_idle(); 

至此,我們發現,其余 CPU 的 IDLE 進程也是和第一顆 CPU 的 IDLE 進程做著一樣的事情,即不斷死循環進行進程調度,最終目的都是為了當前 CPU 一直可以有機器指令在跑。

2.5.3 IDLE 進程調度小結

  • 內核的核心初始化流程是由第一顆 CPU 來做的,在這個流程中,第一個 IDLE 進程創建了 1 號進程和 2 號進程。
  • 所有用戶空間的祖先進程都是 1 號進程,也叫 INIT 進程,我們熟悉的 "僵尸進程" 最后都會被 INIT 進程給清理。
  • INIT 進程還給其余 CPU 創建了 IDLE 進程。
  • IDLE 進程帶有一個死循環邏輯,持續不斷嘗試進程調度,為的就是 CPU 上一直可以有機器指令在執行。

2.6 進程調度時機小結

  • 系統調用 yield、pause 會使得當前進程讓出 CPU,隨后進行一次進程調度。
  • 系統調用 futex(wait) 等待某個信號量,將進程設置為 TASK_INTERRUPTIBLE 狀態,然后進行一次進程調度。
  • 進程在退出的時候,會系統調用到 exit 方法,將當前進程設置為 TASK_DEAD 之后,進行一次進程調度。
  • 在創建新進程、喚醒進程、周期調度過程中,內核會給當前進程設置一個需要調度的標志,然后在下一次中斷返回到用戶空間時,進行一次調度。

3 本文總結

我們通常意識上的進程在 Linux 內核中的實體是由 task_struct 來承載,這個數據結構有進程所有的信息。

0 號進程,即 IDLE 進程是在代碼中靜態定義的,是所有進程的祖先,它創造了 1 號進程,也就是 INIT 進程,這個進程是所有用戶空間進程的祖先。

在一些系統調用過程中,會直接觸發進程調度,在另一些系統調用中,會設置需要調度的標志,以便中斷返回時進行一次進程調度。

內核也會周期性地進行調度,其中一個是周期性地給進程設置需要調度的標志,另一個就是 IDLE 進程不斷嘗試調度。

4 結語

本來這篇文章的規劃是將進程切換的核心邏輯也包含在內的,沒想到光是前面一部分就耗費了如此多的篇幅,所以進程切換的詳細邏輯就放在下一篇文章中寫了。

進程切換的邏輯非常有意思:包括如何切換虛擬內存,切換寄存器和棧,甚至在多個 CPU 之間進行負載均衡等等。歡迎大家關注后續的 Linux 內核系列文章。

本文轉載自微信公眾號「閃電俠的博客」,可以通過以下二維碼關注。轉載本文請聯系閃電俠的博客公眾號。

 

 

責任編輯:武曉燕 來源: 閃電俠的博客
相關推薦

2009-12-11 09:42:54

Linux內核源碼進程調度

2009-12-11 09:47:23

Linux內核源碼進程調度

2010-03-08 14:40:27

Linux進程調度

2023-05-08 12:03:14

Linux內核進程

2022-03-11 20:23:14

鴻蒙源碼分析進程管理

2012-05-14 14:09:53

Linux內核調度系統

2023-03-03 00:03:07

Linux進程管理

2021-04-15 05:51:25

Linux

2023-11-26 18:54:29

Linux調度器

2021-05-20 09:50:20

鴻蒙HarmonyOS應用

2021-05-12 07:50:02

CFS調度器Linux

2023-03-05 15:28:39

CFSLinux進程

2022-06-13 14:31:02

資源調度鴻蒙

2023-04-26 15:36:51

WPA鴻蒙

2020-07-28 08:54:39

內核通信Netlink

2017-03-17 15:05:05

Linux內核源碼do_fork

2021-05-13 09:47:08

鴻蒙HarmonyOS應用

2009-09-16 08:40:53

linux進程調度linuxlinux操作系統

2025-06-16 05:10:00

2022-03-03 18:28:28

Harmony進程任務管理模塊
點贊
收藏

51CTO技術棧公眾號

在线观看区一区二| 久久久噜噜噜久噜久久综合| 久久视频在线视频| www.com日本| 天堂中文在线播放| 中文一区一区三区高中清不卡| 国产中文欧美精品| 国产一级视频在线| 狠狠综合久久av一区二区蜜桃| 欧美精品一级二级| 搞av.com| 91在线高清| 粉嫩av亚洲一区二区图片| 茄子视频成人在线| 国产精品 欧美激情| 奇米777国产一区国产二区| 精品视频免费在线| 国产精品国产亚洲精品看不卡| 国产福利电影在线| 国产成人久久精品77777最新版本| 国产91精品久久久久| 亚洲熟女毛茸茸| 人人网欧美视频| 91精品国产综合久久久蜜臀图片| 人妻内射一区二区在线视频 | 黄色精品视频在线观看| 欧美三级电影在线| 欧美一区二区三区日韩| www日韩在线观看| 成人爽a毛片免费啪啪动漫| 国产三级欧美三级| 精选一区二区三区四区五区| 国产女人高潮时对白| 亚洲女同在线| 欧美精品久久久久| 国产午夜精品理论片| 日韩精品社区| 亚洲大胆美女视频| 下面一进一出好爽视频| 成人一级视频| 色综合久久久久综合体桃花网| 欧美美女黄色网| 日本视频不卡| 中文字幕第一区综合| 久久久久资源| 天天操天天干天天插| 国产美女娇喘av呻吟久久| 国产玖玖精品视频| 无码人妻久久一区二区三区| 国产亚洲精品bv在线观看| 欧美成人精品一区二区| 久久国产高清视频| 日韩欧美字幕| 色婷婷**av毛片一区| 国产在线综合视频| 超碰成人久久| 亚洲最新av网址| 摸摸摸bbb毛毛毛片| 久久不见久久见免费视频7| 日韩av有码在线| 91在线中文字幕| 国产免费一级视频| 午夜在线精品偷拍| 69视频在线播放| 国产成人无码精品亚洲| 亚洲福利一区| 97在线免费观看| 天堂网一区二区三区| 日韩午夜免费视频| 日本精品视频在线观看| 男人天堂视频在线| 午夜av在线免费观看| 牛牛精品成人免费视频| 欧美久久婷婷综合色| 午夜久久久精品| 日韩国产一二三区| 欧美精品色一区二区三区| 欧美专区第二页| www.神马久久| 亚洲精品一二区| 精品人妻无码一区| 国产精品97| 欧美黄网免费在线观看| 精品91久久久| 日韩精品电影一区亚洲| 国产日韩中文字幕在线| www.黄色一片| 91亚洲精华国产精华精华液| 欧美亚洲爱爱另类综合| 在线观看免费黄色| 亚洲综合av网| 免费日韩视频在线观看| 9999在线精品视频| 亚洲福利视频在线| 人妻视频一区二区| 欧美a级片网站| 4438全国亚洲精品在线观看视频| 波多野结衣电影在线播放| 久久99精品国产.久久久久久| 91精品婷婷国产综合久久蝌蚪| 四虎精品一区二区三区| 国产精品视频你懂的| 老司机午夜网站| 国模冰冰炮一区二区| 欧美一区二区三区视频| 国产精品探花一区二区在线观看| 久久五月天小说| 久久久久免费精品国产| 国产精品xxxxxx| 成人午夜视频免费看| 亚洲国产午夜伦理片大全在线观看网站 | 成人免费无码大片a毛片| 日本久久黄色| 97久久精品人人澡人人爽缅北| 国产九色91回来了| 不卡一区二区中文字幕| 中文字幕av导航| 成人香蕉视频| 欧美电影精品一区二区| 怡红院一区二区三区| 最新成人av网站| 91麻豆国产精品| 国产午夜视频在线观看| 亚洲国产aⅴ天堂久久| 久热在线视频观看| 国产精品三级| 97国产真实伦对白精彩视频8| 91资源在线视频| 国产人成亚洲第一网站在线播放 | 国产精品日本欧美一区二区三区| 成人性教育视频在线观看| 久久久久久久影院| 成品人视频ww入口| 在线播放亚洲一区| 免费大片在线观看| 亚洲天堂av资源在线观看| 亚洲欧美日本精品| 国产在线一二区| 国产乱子轮精品视频| 五月天婷亚洲天综合网鲁鲁鲁| 超碰成人av| 日韩欧美卡一卡二| 久久精品一区二区三区四区五区 | 成人手机在线播放| 香蕉成人影院| 亚洲午夜精品久久久久久久久久久久 | 玖玖精品一区| 久久综合五月天| 136福利视频导航| 国产精品视频一二三| av在线无限看| 国产在线观看91一区二区三区 | 男女做暖暖视频| 天天射天天干天天| 九一久久久久久| 亚洲精品中文综合第一页| 台湾佬中文娱乐久久久| 亚洲视频第一页| 久久久成人免费视频| 久久午夜色播影院免费高清 | 日韩中文字幕区一区有砖一区| 久久久久网址| 写真福利精品福利在线观看| 尤物yw午夜国产精品视频明星| 黄色一区二区视频| 中文字幕一区二区三区在线播放| 亚洲另类第一页| 亚洲乱码精品| 国产精品视频免费观看| 91吃瓜在线观看| 日韩黄色高清视频| 日本中文字幕久久| 国产精品嫩草99a| 国产性生活一级片| 亚洲无吗在线| 国产一级精品aaaaa看| 成人免费看视频网站| 一本色道久久综合亚洲精品小说| 影音先锋国产在线| 亚洲精品乱码久久久久久黑人| 污污免费在线观看| 久久亚洲精品伦理| 自拍偷拍99| 高清精品xnxxcom| 国产成人午夜视频网址| 一区二区高清不卡| 欧美成人福利视频| 欧美黑人一区二区| 亚洲欧美在线视频观看| 人妻换人妻a片爽麻豆| 日韩二区三区四区| www.国产亚洲| 国产欧美久久一区二区三区| 成人在线观看视频网站| 狼人综合视频| 中文字幕亚洲情99在线| 欧美 日韩 国产 成人 在线 91| 一本久久a久久精品亚洲| 黄色录像免费观看| 久久蜜桃av一区精品变态类天堂 | 老司机午夜精品99久久| 日本大片免费看| 精品免费视频| 99国产在线| 亚洲伦乱视频| 久久久午夜视频| 在线观看麻豆| 亚洲精品国产精品国自产观看浪潮 | 一区二区三区产品免费精品久久75| 在线观看国产网站| 狠狠色综合播放一区二区| 成年人视频观看| 午夜久久一区| 亚洲三区在线| 五月综合久久| 99视频在线播放| 黄页免费欧美| 欧美一二三视频| 女子免费在线观看视频www| 中文字幕日韩精品在线| 五月婷在线视频| 日韩女优毛片在线| 亚洲最新av网站| 欧洲av在线精品| 国产精品100| 亚洲成人黄色影院| 精品国产视频在线观看| 欧美极品另类videosde| 六十路息与子猛烈交尾| 国产成人在线观看| 国产一级免费大片| 麻豆精品新av中文字幕| 久久精品午夜福利| 国产欧美日本| 国产美女在线一区| 亚洲婷婷免费| 日韩a级黄色片| 综合一区av| 日韩国产精品毛片| 国产精品久久久久久久免费观看 | 亚洲国产精品成人| 一区二区不卡在线观看| 日韩一区电影| 亚洲福利av| 日韩啪啪电影网| 亚洲综合第一| 91影院成人| 伊人久久大香线蕉精品| 久久理论电影| 中文字幕在线观看一区二区三区| 色97色成人| 最新欧美日韩亚洲| 综合激情在线| av一区二区三区免费观看| 国产精品99一区二区| 欧美高清中文字幕| 91精品国产视频| 欧美美女黄色网| 亚洲人妖在线| 激情六月丁香婷婷| 日韩电影在线免费| 韩国视频一区二区三区| 老司机一区二区| 国产精品19p| 成人免费高清视频| 给我免费观看片在线电影的| 91麻豆文化传媒在线观看| 强伦人妻一区二区三区| 国产精品女主播在线观看| 91高清免费观看| 亚洲国产欧美在线人成| 国产九色在线播放九色| 欧美性一二三区| 国产有码在线观看| 精品999久久久| 青青操视频在线| 最新日韩中文字幕| 日本在线视频www鲁啊鲁| 66m—66摸成人免费视频| 三级成人在线| 91在线免费观看网站| 成人性生交大片免费看96| 久久影院理伦片| 四季av一区二区凹凸精品| 成人国产在线看| 欧美一级专区| 国产毛片久久久久久| 成人av在线影院| 一级黄色录像毛片| 一区二区三区四区av| 日韩色图在线观看| 51午夜精品国产| 三级av在线| 久久亚洲影音av资源网| 午夜影院在线播放| 成人性生交xxxxx网站| 少妇高潮一区二区三区| 亚洲精品成人自拍| 亚洲激情av| 99re6在线观看| 久久亚洲一区二区三区明星换脸| а天堂中文在线资源| 午夜日韩在线观看| 91尤物国产福利在线观看| 亚洲美女激情视频| 色yeye免费人成网站在线观看| 国产精品看片资源| 欧美黑人巨大videos精品| 在线成人性视频| 美女尤物久久精品| 好吊操视频这里只有精品| 国产女人aaa级久久久级| 国产精品成人久久| 欧美精品成人一区二区三区四区| 欧美精品少妇| 久久久久久亚洲| 国产一区 二区| 亚洲国产精品123| 亚洲免费中文| 亚洲成av人片在线观看无| 日韩美女久久久| 亚洲国产无线乱码在线观看| 日韩大陆欧美高清视频区| caoporm免费视频在线| 国产精品一区二区三区毛片淫片| 人人网欧美视频| 丝袜人妻一区二区三区| 国产精品白丝jk黑袜喷水| 制服丨自拍丨欧美丨动漫丨| 色哟哟国产精品免费观看| 日本黄色一区二区三区| 欧美国产日韩xxxxx| 91久久青草| 亚洲欧洲国产精品久久| 日韩影院精彩在线| 欧美18—19性高清hd4k| 欧美天堂在线观看| 午夜视频福利在线| 性欧美激情精品| 99久久免费精品国产72精品九九| 香蕉视频免费版| 紧缚奴在线一区二区三区| www.日本高清视频| 欧美色视频一区| 在线观看a视频| 国产精品欧美激情| 日本久久一二三四| 鲁一鲁一鲁一鲁一av| 中文字幕第一区综合| 中文字幕网址在线| 最近2019年中文视频免费在线观看 | 首页亚洲中字| 精品这里只有精品| www一区二区| 四虎成人在线观看| 亚洲色无码播放| 91国内外精品自在线播放| 亚洲精品在线视频观看| 精品一区二区在线看| 91视频综合网| 精品精品国产高清a毛片牛牛| 色yeye免费人成网站在线观看| 国产精品二区三区四区| 亚洲精品男同| 最新中文字幕视频| 欧美三级中文字| 日本不卡不卡| 51国偷自产一区二区三区| 欧美色综合网| 亚洲精品女人久久久| 欧美性大战xxxxx久久久| 91caoporn在线| 亚洲自拍小视频| 一区在线免费| 国产人妻大战黑人20p| 欧美日韩中文精品| 亚洲色图美国十次| 久久涩涩网站| 麻豆精品在线视频| 黑人巨大精品一区二区在线| 亚洲国产精品va在线观看黑人| 久久夜夜操妹子| 欧美aaa在线观看| 99精品国产视频| 亚洲一级在线播放| 久久91精品国产| 亚洲裸色大胆大尺寸艺术写真| 亚洲黄色av网址| 亚洲综合999| h视频网站在线观看| 999国产在线| 日一区二区三区| 国产人妻精品一区二区三区不卡| 日韩av综合网站| 亚洲精品毛片| 人人妻人人添人人爽欧美一区| 国产精品色呦呦| 五月天久久久久久| 亚洲一区美女视频在线观看免费| 亚洲免费综合|