九色91_成人精品一区二区三区中文字幕_国产精品久久久久一区二区三区_欧美精品久久_国产精品99久久久久久久vr_www.国产视频

Hello! 歡迎來到小浪云!


聊聊Linux中線程和進程的聯系與區別!


avatar
小浪云 2025-01-05 159

關于進程和線程,在linux中是非常核心的概念。然而,很多人對它們之間的聯系和區別并不清楚。

在網上對進程和線程的討論中,大多數集中在它們之間的差異。但實際上,在Linux系統中,進程和線程的相似之處要遠遠多于它們的不同之處。在Linux環境下,線程甚至被稱為輕量級進程。

今天,我將從Linux內核實現的角度,深入比較進程和線程。

一、線程的創建方式

redis 6.0以上的版本為例,它開始支持使用多線程提供核心服務。

一旦Redis主線程啟動,就會調用initThreadedIO函數來創建多個I/O線程。

?

redis 源碼地址:https://github.com/redis/redis

//file:src/networking.c void?initThreadedIO(void)?{ ?//開始?io?線程的創建 ?for?(int?i?=?0;?i?

創建線程具體調用的是 pthread_create 函數,pthread_create 是在 glibc 庫中實現的。在 glibc 庫中,pthread_create 函數的實現調用路徑是 __pthread_create_2_1 -> create_thread。其中 create_thread 這個函數比較重要,它設置了創建線程時使用的各種 flag 標記。

//file:nptl/sysdeps/pthread/createthread.c static?int create_thread?(struct?pthread?*pd,?...) { ?int?clone_flags?=?(CLONE_VM?|?CLONE_FS?|?CLONE_FILES?|?CLONE_SIGNAL ????|?CLONE_SETTLS?|?CLONE_PARENT_SETTID ????|?CLONE_CHILD_CLEARTID?|?CLONE_SYSVSEM ????|?0);  ?int?res?=?do_clone?(pd,?attr,?clone_flags,?start_thread, ??????STACK_VARIABLES_ARGS,?1); ?... } 

在上面的代碼中,傳入參數中的各個 flag 標記是非常關鍵的。這里我們先知道一下傳入了 CLONE_VM、CLONE_FS、CLONE_FILES 等標記就行了,后面我們會講內核中針對這些參數做的特殊處理。

接下來的 do_clone 最終會調用一段匯編程序,在匯編里進入 clone 系統調用,之后會進入內核中進行處理。

//file:sysdeps/unix/sysv/Linux/i386/clone.S ENTRY?(BP_SYM?(__clone)) ?... ?movl?$SYS_ify(clone),%eax ?... 

二、內核中對線程的表示

在開始介紹線程的創建過程之前,先給大家看看內核中表示線程的數據結構

開篇的時候我說了,進程和線程的相同點要遠遠大于不同點。主要依據就是在 Linux 中,無論進程還是線程,都是抽象成了 task 任務,在源碼里都是用 task_struct 結構來實現的。

聊聊Linux中線程和進程的聯系與區別!

我們來看 task_struct 具體的定義,它位于 include/linux/sched.h

//file:include/linux/sched.h struct?task_struct?{ ?//1.1?task狀態? ?volatile?long?state;  ?//1.2?進程線程的pid ?pid_t?pid; ?pid_t?tgid;  ?//1.3?task樹關系:父進程、子進程、兄弟進程 ?struct?task_struct?__rcu?*parent; ?struct?list_head?children;? ?struct?list_head?sibling; ?struct?task_struct?*group_leader;?  ?//1.4?task調度優先級 ?int?prio,?static_prio,?normal_prio; ?unsigned?int?rt_priority;  ?//1.5?地址空間 ?struct?mm_struct?*mm,?*active_mm;  ?//1.6?文件系統信息(當前目錄等) ?struct?fs_struct?*fs;  ?//1.7?打開的文件信息 ?struct?files_struct?*files;  ?//1.8?namespaces? ?struct?nsproxy?*nsproxy;  ?... } 

這個數據結構已經在上一篇文章《Linux進程是如何創建出來的?》中,我們詳細介紹過了。

對于線程來講,所有的字段都是和進程一樣的(本來就是一個結構體來表示的)。包括狀態、pid、task 樹關系、地址空間、文件系統信息、打開的文件信息等等字段,線程也都有。

這也就是我前面說的,進程和線程的相同點要遠遠大于不同點,本質上是同一個東西,都是一個 task_struct !正因為進程線程如此之相像,所以在 Linux 下的線程還有另外一個名字,叫輕量級進程。至于說輕量在哪兒,稍后我們再說。

這里我們稍微說一下 pid 和 tgid 這兩個字段。在 Linux 中,每一個 task_struct 都需要被唯一的標識,它的 pid 就是唯一標識號。

//file:include/linux/sched.h struct?task_struct?{ ?...... ?pid_t?pid; ?pid_t?tgid; } 

對于進程來說,這個 pid 就是我們平時常說的進程 pid。

對于線程來說,我們假如一個進程下創建了多個線程出來。那么每個線程的 pid 都是不同的。但是我們一般又需要記錄線程是屬于哪個進程的。這時候,tgid 就派上用場了,通過 tgid 字段來表示自己所歸屬的進程 ID。

聊聊Linux中線程和進程的聯系與區別!

這樣內核通過 tgid 可以知道線程屬于哪個進程。

三、線程創建過程

要想知道進程和線程的區別到底在哪兒,我們從線程的創建過程來詳細看一下。

3.1 回顧進程創建

在《Linux進程是如何創建出來的?》一文中我們了解了進程的創建過程。事實上,進程線程創建的時候,使用的函數看起來不一樣。但實際在底層實現上,最終都是使用同一個函數來實現的。

聊聊Linux中線程和進程的聯系與區別!

我們再簡單回顧一下創建進程時 fork 系統調用的源碼,fork 調用主要就是執行了 do_fork 函數。注意:fork 函數調用 do_fork 的傳的參數分別是SIGCHLD、0,0,NULL,NULL

//file:kernel/fork.c SYSCALL_DEFINE0(fork) { ?return?do_fork(SIGCHLD,?0,?0,?NULL,?NULL); } 

do_fork 函數又調用 copy_process 完成進程的創建。

//file:kernel/fork.c long?do_fork(...) { ?//復制一個?task_struct?出來 ?struct?task_struct?*p; ?p?=?copy_process(clone_flags,?...); ?... } 

3.2 線程的創建

我們在本文第一小節里介紹到 lib 庫函數 pthread_create 會調用到 clone 系統調用,為其傳入了一組 flag。

//file:nptl/sysdeps/pthread/createthread.c static?int create_thread?(struct?pthread?*pd,?...) { ?int?clone_flags?=?(CLONE_VM?|?CLONE_FS?|?CLONE_FILES?|?CLONE_SIGNAL ????|?CLONE_SETTLS?|?CLONE_PARENT_SETTID ????|?CLONE_CHILD_CLEARTID?|?CLONE_SYSVSEM ????|?0);  ?int?res?=?do_clone?(pd,?attr,?clone_flags,?...); ?... } 

好,我們找到 clone 系統調用的實現。

//file:kernel/fork.c SYSCALL_DEFINE5(clone,?......) { ?return?do_fork(clone_flags,?newsp,?0,?parent_tidptr,?child_tidptr); } 

同樣,do_fork 函數還是會執行到 copy_process 來完成實際的創建。

3.3 進程線程創建異同

可見和創建進程時使用的 fork 系統調用相比,創建線程的 clone 系統調用幾乎和 fork 差不多,也一樣使用的是內核里的 do_fork 函數,最后走到 copy_process 來完整創建。

不過創建過程的區別是二者在調用 do_fork 時傳入的 clone_flags 里的標記不一樣!。

  • 創建進程時的 flag:僅有一個 SIGCHLD
  • 創建線程時的 flag:包括 CLONE_VM、CLONE_FS、CLONE_FILES、CLONE_SIGNAL、CLONE_SETTLS、CLONE_PARENT_SETTID、CLONE_CHILD_CLEARTID、CLONE_SYSVSEM。

關于這些 flag 的含義,我們選幾個關鍵的做一個簡單的介紹,后面介紹 do_fork 細節的時候會再次涉及到。

  • CLONE_VM: 新 task 和父進程共享地址空間
  • CLONE_FS:新 task 和父進程共享文件系統信息
  • CLONE_FILES:新 task 和父進程共享文件描述符表

這些 flag 會對 task_struct 產生啥影響,我們接著看接下來的內容。

四、揭秘 do_fork 系統調用

在本節中我們以動態的視角來看一下線程的創建過程.

前面我們看到,進程和線程創建都是調用內核中的 do_fork 函數來執行的。在 do_fork 的實現中,核心是一個 copy_process 函數,它以拷貝父進程(線程)的方式來生成一個新的 task_struct 出來。

//file:kernel/fork.c long?do_fork(unsigned?long?clone_flags,?...) { ?//復制一個?task_struct?出來 ?struct?task_struct?*p; ?p?=?copy_process(clone_flags,?stack_start,?stack_size, ????child_tidptr,?NULL,?trace);  ?//子任務加入到就緒隊列中去,等待調度器調度 ?wake_up_new_task(p); ?... } 

在創建完畢后,調用 wake_up_new_task 將新創建的任務添加到就緒隊列中,等待調度器調度執行。這個代碼很長,我對其進行了一定程度的精簡。

//file:kernel/fork.c static?struct?task_struct?*copy_process(...) { ?//4.1?復制進程?task_struct?結構體 ?struct?task_struct?*p; ?p?=?dup_task_struct(current); ?...  ?//4.2?拷貝?files_struct ?retval?=?copy_files(clone_flags,?p);  ?//4.3?拷貝?fs_struct ?retval?=?copy_fs(clone_flags,?p);  ?//4.4?拷貝?mm_struct ?retval?=?copy_mm(clone_flags,?p);  ?//4.5?拷貝進程的命名空間?nsproxy ?retval?=?copy_namespaces(clone_flags,?p);  ?//4.6?申請?pid?&&?設置進程號 ?pid?=?alloc_pid(p->nsproxy->pid_ns); ?p->pid?=?pid_nr(pid); ?p->tgid?=?p->pid; ?if?(clone_flags?&?CLONE_THREAD) ??p->tgid?=?current->tgid;  ?...... } 

可見,copy_process 先是復制了一個新的 task_struct 出來,然后調用 copy_xxx 系列的函數對 task_struct 中的各種核心對象進行拷貝處理,還申請了 pid 。接下來我們分小節來查看該函數的每一個細節。

4.1 復制 task_struct 結構體

注意一下,上面調用 dup_task_struct 時傳入的參數是 current,它表示的是當前任務。在 dup_task_struct 里,會申請一個新的 task_struct 內核對象,然后將當前任務復制給它。需要注意的是,這次拷貝只會拷貝 task_struct 結構體本身,它內部包含的 mm_struct 等成員不會被復制。

聊聊Linux中線程和進程的聯系與區別!

我們來簡單看下具體的代碼。

//file:kernel/fork.c static?struct?task_struct?*dup_task_struct(struct?task_struct?*orig) { ?//申請?task_struct?內核對象 ?tsk?=?alloc_task_struct_node(node); ?//復制?task_struct ?err?=?arch_dup_task_struct(tsk,?orig); ?... } 

其中 alloc_task_struct_node 用于在 slab 內核內存管理區中申請一塊內存出來。關于 slab 機制請參考- 內核內存管理

//file:kernel/fork.c static?struct?kmem_cache?*task_struct_cachep; static?inline?struct?task_struct?*alloc_task_struct_node(int?node) { ?return?kmem_cache_alloc_node(task_struct_cachep,?GFP_KERNEL,?node); } 

申請完內存后,調用 arch_dup_task_struct 進行內存拷貝。

//file:kernel/fork.c int?arch_dup_task_struct(struct?task_struct?*dst, ?????????struct?task_struct?*src) { ?*dst?=?*src; ?return?0; } 

4.2 拷貝打開文件列表

我們先回憶一下前面的內容,創建線程調用 clone 系統調用的時候,傳入了一的 flag,其中有一個就是 CLONE_FILES。如果傳入了 CLONE_FILES 標記,就會復用當前進程的打開文件列表 – files 成員。

聊聊Linux中線程和進程的聯系與區別!

對于創建進程來講,沒有傳入這個標志,就會新創建一個 files 成員出來。

聊聊Linux中線程和進程的聯系與區別!

好了,我們繼續看 copy_files 具體實現。

//file:kernel/fork.c static?int?copy_files(unsigned?long?clone_flags,?struct?task_struct?*tsk) { ?struct?files_struct?*oldf,?*newf; ?oldf?=?current->files;  ?if?(clone_flags?&?CLONE_FILES)?{ ??atomic_inc(&oldf->count); ??goto?out; ?} ?newf?=?dup_fd(oldf,?&error); ?tsk->files?=?newf; ?... } 

從代碼看出,如果指定了 CLONE_FILES(創建線程的時候),只是在原有的 files_struct 里面 +1 就算是完事了,指針不變,仍然是復用創建它的進程的 files_struct 對象。

這就是進程和線程的其中一個區別,對于進程來講,每一個進程都需要獨立的 files_struct。但是對于線程來講,它是和創建它的線程復用 files_struct 的。

4.3 拷貝文件目錄信息

再回憶一下創建線程的時候,傳入的 flag 里也包括 CLONE_FS。如果指定了這個標志,就會復用當前進程的文件目錄 – fs 成員。

聊聊Linux中線程和進程的聯系與區別!

對于創建進程來講,沒有傳入這個標志,就會新創建一個 fs 出來。

聊聊Linux中線程和進程的聯系與區別!

好,我們繼續看 copy_fs 的實現。

//file:kernel/fork.c static?int?copy_fs(unsigned?long?clone_flags,?struct?task_struct?*tsk) { ?struct?fs_struct?*fs?=?current->fs; ?if?(clone_flags?&?CLONE_FS)?{ ??fs->users++; ??return?0; ?} ?tsk->fs?=?copy_fs_struct(fs); ?return?0; } 

和 copy_files 函數類似,在 copy_fs 中如果指定了 CLONE_FS(創建線程的時候),并沒有真正申請獨立的 fs_struct 出來,近幾年只是在原有的 fs 里的 users +1 就算是完事。

而在創建進程的時候,由于沒有傳遞這個標志,會進入到 copy_fs_struct 函數中申請新的 fs_struct 并進行賦值拷貝。

4.4 拷貝內存地址空間

創建線程的時候帶了 CLONE_VM 標志,而創建進程的時候沒帶。接下來在 copy_mm 函數 中會根據是否有這個標志來決定是該和當前線程共享一份地址空間 mm_struct,還是創建一份新的。

//file:kernel/fork.c static?int?copy_mm(unsigned?long?clone_flags,?struct?task_struct?*tsk) { ?struct?mm_struct?*mm,?*oldmm; ?oldmm?=?current->mm;  ?if?(clone_flags?&?CLONE_VM)?{ ??atomic_inc(&oldmm->mm_users); ??mm?=?oldmm; ??goto?good_mm; ?} ?mm?=?dup_mm(tsk); good_mm: ?return?0;? } 

對于線程來講,由于傳入了 CLONE_VM 標記,所以不會申請新的 mm_struct 出來,而是共享其父進程的。

聊聊Linux中線程和進程的聯系與區別!

多線程程序中的所有線程都會共享其父進程的地址空間。

聊聊Linux中線程和進程的聯系與區別!

而對于多進程程序來說,每一個進程都有獨立的 mm_struct(地址空間)。

聊聊Linux中線程和進程的聯系與區別!

因為在內核中線程和進程都是用 task_struct 來表示,只不過線程和進程的區別是會和創建它的父進程共享打開文件列表、目錄信息、虛擬地址空間等數據結構,會更輕量一些。所以在 Linux 下的線程也叫輕量級進程

在打開文件列表、目錄信息、內存虛擬地址空間中,內存虛擬地址空間是最重要的。因此區分一個 Task 任務該叫線程還是該叫進程,一般習慣上就看它是否有獨立的地址空間。如果有,就叫做進程,沒有,就叫做線程。

這里展開多說一句,對于內核任務來說,無論有多少個任務,其使用地址空間都是同一個。所以一般都叫內核線程,而不是內核進程。

五 結論

創建線程的整個過程我們就介紹完了?;仡^總結一下,對于線程來講,其地址空間 mm_struct、目錄信息 fs_struct、打開文件列表 files_struct 都是和創建它的任務共享的。

聊聊Linux中線程和進程的聯系與區別!

但是對于進程來講,地址空間 mm_struct、掛載點 fs_struct、打開文件列表 files_struct 都要是獨立擁有的,都需要去申請內存并初始化它們。

聊聊Linux中線程和進程的聯系與區別!

總之,在 Linux 內核中并沒有對線程做特殊處理,還是由 task_struct 來管理。從內核的角度看,用戶態的線程本質上還是一個進程。只不過和普通進程比,稍微“輕量”了那么一些。

那么線程具體能輕量多少呢?我之前曾經做過一個進程和線程的上下文切換開銷測試。進程的測試結果是一次上下文切換平均 2.7 – 5.48 us 之間。線程上下文切換是 3.8 us左右??偟膩碚f,進程線程切換還是沒差太多。

相關閱讀

主站蜘蛛池模板: 久久99视频精品 | 国产精品污www一区二区三区 | 九九视频网 | 亚洲精品在线免费观看视频 | 毛片免费看的 | 免费一级网站 | 亚洲国产精品一区二区第一页 | 亚洲国产精品成人无久久精品 | 国产一区日韩在线 | 婷婷久久综合 | 在线视频亚洲 | 99影视| jⅰzz亚洲| 国产综合视频 | 国产精品久久 | 国产精品自拍视频网站 | 91精品综合久久久久久五月天 | 国产精品久久久久久久久久久久冷 | 精品国产精品一区二区夜夜嗨 | 亚洲日本欧美 | 精品久久久久久久久久久院品网 | 国产十日韩十欧美 | 91视频在线看 | 四虎影院在线播放 | 国产欧美日韩一区 | 99久久久无码国产精品 | 日操操 | 在线亚洲一区 | 亚洲一二三区精品 | 九九热这里只有精品在线观看 | 亚洲电影免费 | 中文字幕亚洲欧美 | 久久极品| 国产精品久久久久久久岛一牛影视 | 国产精品呻吟久久av凹凸 | 一级毛片黄片 | 国产日本精品视频 | 日韩中文一区二区 | 97国产一区二区 | 毛片视频免费观看 | 成年人网站在线观看视频 |