背景
對進程和線程的理解,之前一直都是憑一些零碎不完整的信息在理解;
linux的進程和線程基本上一樣,線程是輕量級進程,彼此有關聯又獨立。
得虧內核支持的好,寫用戶態程序可以不依賴于實現的理解,只需要知道從哪個函數開始就是線程函數了,哪里的變量是全局的,哪里的變量是線程私有的。
這樣寫出來的程序也不至于有問題。
一直覺得沒有把這個東西搞清楚,終于受不了這種煎熬決定看下底層是怎么實現的。
線程/進程歷史
linux系統是先有進程的概念,后有線程的概念;
linux內核里面現有的線程實現,其實是redhat這個發行版廠商的實現,合入到linux主線了,redhat的實現就是現在的glibc里面的nptl(Native?POSIX Thread Library)加上內核fork共同協作實現的
除了NPTL,還有IBM的開源實現NGPT,后面沒有被納入內核,停止維護;
Linux內核也實現了Linux Threads,對比NPTL優勢不明顯吧
Linux線程實現概述
用戶態(glibc)+內核態組合共同實現
用戶態做的事情
線程attribute賦值(如果需要)
線程運行時用戶態運行的棧空間分配(包括棧空間的緩存和分配,大小、棧地址)
glibc的線程對象創建以及初始化
內核態做的事情
clone系統調用在調用do_fork時,主要flag有:CLONE_VM | CLONE_FS | CLONE_FILES
CLONE_VM表示和創建線程的進程共享地址空間
CLONE_FS表示和創建線程的進程共享文件系統(根目錄、工作目錄、文件創建掩碼等)
CLONE_FILES表示和創建線程的進程共享打開的文件
創建線程用的clone和創建真進程fork的區別是什么
fork在內核也是調用do_fork函數,fork系統調用在調用do_fork創建新的進程時,只攜帶了SIGCHLD這一個flag,其實就是不共享地址空間、打開的文件、使用的文件系統等,其它的和clone基本沒有區別。
說線程是輕量級進程就是這個原因,和創建它的進程共享了一部分主要資源。
后文把線程、輕量級進程、進程統一稱為進程,linux系統里面進程是內核支持的,線程是輕量級進程的別名(我認為這樣好理解,沒有官方說法支持)
用戶態和內核態如何配合達到效果
進程是對CPU的虛擬化,CPU是用來執行二進制程序的,二進制可以是用戶態也可以是內核態;也就是進程既可以執行用戶態代碼,也可以執行內核態代碼,進程是一個邏輯概念,內核態/用戶態代碼是表示運行指令時CPU處在不同的保護模式(ring0還是ring3)
進程是由內核態創建,進程主要作用是做為調度一員,獲取CPU時間片、地址空間、文件系統、打開文件、進程權限、用戶權限、cpu親和性、調度算法、進程組、運行時間、命名空間等信息(其它信息可以看task_struct的定義)
進程如何創建和運行起來
long do_fork(...)
{struct task_struct *p;p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace, NUMA_NO_NODE);wake_up_new_task(p);}
依據clone_flags(用戶態或者系統調用時添加的flags)的不同使用copy_process創建一個新的進程p;
新創建的進程p執行了wake_up_new_task后就會被調度進程調度運行,進程被調度就會分配CPU運行。
如何執行用戶態線程函數
用戶態線程函數如何觸發執行
glibc里面pthread_create在X86_64的實現里面會調用__clone函數(clone.S,clone的實現之一)
1、用戶線程函數地址調用pthread_create后如何保存
用戶調用glibc函數創建線程時傳遞下來的fn地址是放在rdi這個寄存器(clone.S注釋有說明,應該也是遵循ABI函數調用堆棧參數保存約定的)。
注意這里的fn其實是glibc的start_thread函數,這個函數再調用戶的線程函數,指針保存在start_routine成員里面,具體代碼如下
if (pd->c11){/* The function pointer of the c11 thread start is cast to an incorrecttype on __pthread_create_2_1 call, however it is casted back to correctone so the call behavior is well-defined (it is assumed that pointersto void are able to represent all values of int. */int (*start)(void*) = (int (*) (void*)) pd->start_routine;ret = (void*) (uintptr_t) start (pd->arg);}
elseret = pd->start_routine (pd->arg);
2、fn如何被調用
fn是用戶態地址,確實沒有必要傳遞到內核函數里面在內核態調用,這樣就牽扯到地址轉換了
那如何調用?在clone系統調用之前,把fn地址壓到子進程的棧里面,系統調用返回后,再彈出地址,通過call執行函數,相關代碼為:
ENTRY (__clone)......movq %rdi,0(%rsi)............movl $SYS_ify(clone),%eax....../* End FDE now, because in the child the unwind info will bewrong. */cfi_endproc;syscalltestq %rax,%raxjl SYSCALL_ERROR_LABELjz L(thread_start)retL(thread_start):popq %rax /* Function to call. */popq %rdi /* Argument. */call *%rax
內核態如何找到用戶態的棧
glibc創建棧空間后,棧起始地址以及棧大小會通過系統調用的參數傳遞到內核
(如上面copy_process函數的第二個和第三個參數)
接下來內核調用copy_thread(clone_flags, stack_start, stack_size, p)(不同的架構對應不同的實現)
*childregs = *current_pt_regs();childregs->ax = 0;
if (sp)childregs->sp = sp;
這里對應的是X86_64的實現,可以看到把用戶態傳下來的棧指針設置到棧寄存器里面
這里有個點,這個sp和上面保存fn的棧是一個嗎?是用戶態分配的棧嗎?
我認為是的,這個是已經push過參數和fn之后的棧。
其它
為什么線程的棧要用戶分配在用戶態?
A:棧要么分在用戶態,要么分在內核態
如果分在用戶態,線程主循環函數運行在用戶態,都是在用戶態,就不需要CPU模式切換,運行速度更快,都是用戶態代碼也不存在對內核的安全問題
如果分在內核態,那用戶態函數執行,函數執行要不停操作棧,那至少就會有內核地址到用戶態地址的轉換,要不要牽扯模式切換就另說了。