該文出自:http://www.civilnet.cn/bbs/browse.php?topicno=78429php
首先聲明,gemfield本文以Linux爲基礎,所涉及到的線程概念以Linux爲準。避免對於windows下的你產生困擾。算法
在《從程序到進程》一文中,咱們知道了進程在內核中是以一個task_struct結構來描述和維護的,那麼,咱們編程中使用的線程概念,在內核中是怎麼維護的呢?和進程有什麼區別?編程
真相是:進程和線程沒有什麼大的區別;在Linux內核中,內核將用戶進程、用戶線程和內核線程一視同仁,即內核使用惟一的數據結構task_struct來分別表示他們;內核使用相同的調度算法對這三者進行調度;爲何沒有內核進程呢?gemfield說,你能夠把內核線程叫做內核進程!這又是爲何呢?windows
由於在Linux下,進程和線程都是由task_struct結構描述的,要說區別,就是線程是共享一個進程的內存資源的(因此也被稱爲輕量級進程)。而在內核中,內存原本就是共享的,因此你能夠叫它內核線程,也能夠叫它內核進程,不過習慣上稱之爲kernel threads(內核線程)。bash
gemfield經過ps -ef 命令來給你一個直觀的印象:數據結構
ps -ef 輸出: app
**************************************** dom
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 May13 ? 00:00:05 /sbin/init
root 2 0 0 May13 ? 00:00:00 [kthreadd]
root 3 2 0 May13 ? 00:31:52 [ksoftirqd/0]
root 6 2 0 May13 ? 00:00:39 [migration/0]
root 7 2 0 May13 ? 00:00:33 [watchdog/0]
root 8 2 0 May13 ? 00:00:59 [migration/1]
root 10 2 0 May13 ? 00:26:26 [ksoftirqd/1]
root 11 2 0 May13 ? 00:00:32 [watchdog/1]
root 12 2 0 May13 ? 00:00:43 [migration/2]
root 13 2 0 May13 ? 00:31:13 [kworker/2:0]
root 14 2 0 May13 ? 00:23:13 [ksoftirqd/2]
root 15 2 0 May13 ? 00:00:32 [watchdog/2]
root 16 2 0 May13 ? 00:00:44 [migration/3]
root 18 2 0 May13 ? 00:24:34 [ksoftirqd/3]
root 19 2 0 May13 ? 00:00:37 [watchdog/3]函數
************************************************** post
上面輸出的就是一系列的用戶和內核進程,其中內核線程使用方括號[]括起來。pid是當前進程的id,ppid是父進程的id。 好比:[ksoftirqd/0] 內核線程是用來實施軟中斷的; 更多內容參考:http://civilnet.cn/bbs/topicno/71181。 要了解線程(下面只介紹用戶線程,因此提到線程,指的就是用戶線程)的這些內容,咱們先來看看線程是怎麼產生的?
Linux上使用pthread這個POSIX的線程庫來建立線程。以下:
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(* start_routine)(void *), void *arg);
若成功則返回0,不然返回出錯編號;返回成功時,由thread指向的內存單元被設置爲新建立線程的線程ID;attr參數用於初始化線程屬性;新建立的線程從start_routine函數的地址開始運行,該函數只有一個萬能指針參數arg,若是須要向start_rtn函數傳遞的參數不止一個,那麼須要把這些參數放到一個結構中,而後把這個結構的地址做爲arg的參數傳入。在編譯時注意加上 -lpthread參數,以調用靜態連接庫。由於pthread並不是Linux系統的默認庫。
所以,建立線程的pthread_create就至關於建立進程的程序中的main()函數。
而pthread_create的庫函數調用最終調用了clone()系統調用。如今狀況有點明瞭了:建立進程用的是fork系統調用,而建立線程用的是clone系統調用。焦點就集中在這裏了:fork和clone這兩個系統調用的區別是什麼?
在內核中這2個調用分別調用sys_fork(),sys_clone(),而後又都調用do_fork()去作具體的建立進程的工做。以下:
*****************************************************
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0);
}
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx; newsp = regs.ecx;
if (!newsp)
newsp = regs.esp;//子進程在用戶態時使用的棧低,由clone中的child_stack參數指定
return do_fork(clone_flags, newsp, ®s, 0);
}
****************************************************
這麼說來,建立進程和建立線程的工做最終都歸於do_fork系統調用了?那不是沒有區別了嗎?gemfield說,別急,do_fork系統調用還有參數呢,這個參數是從clone傳過來的。以下:
****************************************************** 標誌 含義
CLONE_PARENT 建立的子進程的父進程是調用者的父進程,新進程與建立它的進程成了「兄弟」而不是「父子」 CLONE_FS 子進程與父進程共享相同的文件系統,包括root、當前目錄、umask
CLONE_FILES 子進程與父進程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS 在新的namespace啓動子進程,namespace描述了進程的文件hierarchy
CLONE_SIGHAND 子進程與父進程共享相同的信號處理(signal handler)表
CLONE_PTRACE 若父進程被trace,子進程也被trace
CLONE_VFORK 父進程被掛起,直至子進程釋放虛擬內存資源
CLONE_VM 子進程與父進程運行於相同的內存空間
CLONE_PID 子進程在建立時PID與父進程一致
CLONE_THREAD Linux 2.4中增長以支持POSIX線程標準,子進程與父進程共享相同的線程羣
************************************************************
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
這裏fn是函數指針,咱們知道進程的4要素,這個就是指向程序的指針, child_stack明顯是爲子進程分配系統堆棧空間,flags就是標誌用來描述你須要從父進程繼承那些資源, arg就是傳給子進程的參數)。而flags能夠取的值就是上面表中所介紹的。
如今咱們知道了這二者的本質區別是參數的不一樣,如今就要去do_fork中去看看是如何處理這些參數的。do_fork的原型:
int do_fork(unsigned int clone_flags, unsigned long stack_start, struct pt_regs * regs, unsigned long stack_size)
一、clone_flags是由2部分組成,最低字節爲信號類型,用於規定子進程去世時向父進程發出的信號,fork中這個信號就是SIGCHLD;
二、clone則能夠由用戶本身定義信號類型。第2部分是表示資源和特性的標誌位(標誌參考上表);
三、對於fork咱們能夠看出第2部分所有是0,也就是對有關資源都要複製,而不是經過指針共享;
do_fork的代碼以下:
**********************************************************
int do_fork(unsigned int clone_flags, unsigned long stack_start, struct pt_regs * regs, unsigned long stack_size)
{
int retval = -ENOMEM;//將可能返回的值error初始值置爲-ENOMEM(error no mem,沒有內存)
struct task_struct *p;//建立一個進程描述符的指針 DECLARE_MUTEX_LOCKED(sem); //定義和建立了一個用於進程互斥和同步的信號量
//CLONE_PID是子進程和父進程擁有相同的PID號,這隻有一種狀況可使用,就是父進程的PID爲0
if(clone_flags & CLONE_PID)
{
if(current->pid)
return -EPERM;
}
current->vfork_sem = sem;
p = alloc_task_struct();//爲子進程分配2個頁面(用來作系統棧和存放task_struct的)
if(!p)
goto fork_out;
*p = *current; //將父進程的task_struct賦值到2個頁面中 retval = -EAGAIN;
//p->user 指向該進程所屬用戶的數據結構,這個數據結構見http://civilnet.cn/bbs/topicno/71182 //內核進程不屬於任何用戶,因此它的p->user = 0),p->rlim是對進程資源的限制, //而p->rlim[RLIMIT_NPROC]則規定了該進程所屬用戶能夠擁有的進程數量,若是超過這個數量就不能夠再fork了。
if(atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur)
goto bad_fork_free;
atomic_inc(&p->user->__count); atomic_inc(&p->user->processes);
//上面是對用戶進程的限制,這裏是對內核進程的數量限制
if(nr_threads >= max_threads) goto bad_fork_cleanup_count;
//p->exec_domain指向一個exec_domain結構,定義見http://civilnet.cn/bbs/topicno/71183 get_exec_domain(p->exec_domain);
//每一個進程都屬於某種可執行的印象格式如a.out或者elf, //對這些格式的支持都是經過動態安裝驅動模塊來實現的,binfmt就是用來指向這些格式驅動。 if(p->binfmt && p->binfmt->module) __MOD_INC_USE_COUNT(p->binfmt->module);
p->did_exec = 0;//表示進程未被執行過 p->swappable = 0;//因爲是新建進程,暫時拒絕被調用出內存 //表示本進程將被置於等待隊列中,因爲資源未分配好, //所以置爲不可中斷,使其待資源有效時喚醒,不可由其它進程經過信號喚醒 p->state = TASK_UNINTERRUPTIBLE; copy_flags(clone_flags, p);
//get_pid()函數先判斷調用它的do_fork()是否進行clone系統調用, //它還進行了與組標識號及區標識號進行區別的判斷; p->pid = get_pid(clone_flags); //設置新建進程的PID
//因爲新產生的進程的狀態仍是爲TASK_UNINTERRUPTIBLE, //所以不將其放入就緒隊列,將next_run,prev_run項均置爲NULL。 //將指向原始父進程、父進程指針項賦值爲當前進程Current; p->run_list.next = NULL; p->run_list.prev = NULL;
if((clone_flags & CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) { p->p_opptr = current; if(!(p->trace & PT_PTRACED)) p->p_pptr = current; }
p->p_cptr = NULL; //wait4()與wait3()函數是一個進程等待子進程完成使命後再繼續執行,這個隊列爲此作準備,這裏是作初始化 init_waitqueue_head(&p->wait_childexit); p->vfork_sem = NULL; spin_lock_init(&p->alloc_lock); //表示新建進程還沒有收到任何信號 p->sigpending = 0; init_sigpending(&p->sigpending); //對子進程待處理信號隊列和有關結構成分初始化 p->it_real_value = p->it_virt_value = p->it_prof_value = 0; p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0; //初始化時間數據成員 init_timer(&p->real_timer); p->real_timer.data = (unsigned long)p; p->leader = 0; p->tty_old_pgrp = 0; p->times.tms_utime = p->times.tms_stime = 0; //對進程各類記時器的初始化 p->times.tms_curtime = p->times.tms_cstime = 0;
#ifdef CONFIG_SMP { int i; p->has_cpu = 0; p->processor = current->processor; for(i = 0; i < smp_num_cpus; i++) p->per_cpu_utime[i] = p->per_cpu_stime[i] = 0; spin_lock_init(&p->sigmask_lock); } #endif //多處理器相關
p->lock_death = -1; p->start_time = jiffies; //對進程初始時間的初始化,jeffies是時鐘中斷記錄的記時器,到這裏task_struct基本初始化完畢 retval = -ENOMEM; //copy_files是複製已打開文件的控制結構,但只有才clone_flags中CLONE_FILES標誌才能進行,不然只是共享 if(copy_files(clone_flags,p)) goto bad_fork_cleanup;
if(copy_fs(clone_flags, p)); goto bad_fork_cleanup_files;
//和上面同樣,這裏是對信號的處理方式 if(copy_sighand(clone_flags, p)) goto bad_fork_cleanpu_fs;
//內存,copy_mm的代碼參考:http://civilnet.cn/bbs/topicno/71184 if(copy_mm(clone_flags, p)) goto bad_fork_cleanup_sighand; //到這裏全部須要有條件複製的資源所有結束
//4個資源中,還剩系統堆棧資源沒有複製, //copy_thread源代碼參考:http://civilnet.cn/bbs/topicno/71185 retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if(retval) goto bad_fork_cleanup_sighand;
p->semundo = NULL; p->parent_exec_id = p->self_exec_id; //parent_exec_id父進程的執行域 p->swappable = 1;//表示本進程的頁面能夠被換出 //將父進程傳入的信號SIGCHLD放入exit_signal,用來被強行終止時發送 p->exit_signal = clone_flags & CSIGNAL; p->pdeath_signal = 0; p->counter = (current->counter + 1) >> 1; current->counter >>= 1;//父進程的分配的時間額被分紅2半
if (!current->counter) current->need_resched = 1; //讓父子進程各擁有時間的一半
retval = p->pid; p->tgid = retval; INIT_LIST_HEAD(&p->thread_group); write_lock_irq(&tasklist_lock);
if (clone_flags & CLONE_THREAD) { p->tgid = current->tgid; list_add(&p->thread_group, ¤t->thread_group); }
SET_LINKS(p); //將子進程的PCB放入進程隊列,讓它能夠接受調度 hash_pid(p); //將子進程放入hash表中 nr_threads++; write_unlock_irq(&tasklist_lock);
if (p->ptrace & PT_PTRACED) send_sig(SIGSTOP, p, 1); //喚醒新進程放入就緒隊列,等待調度,返回 wake_up_process(p); ++total_forks;
fork_out:
if ((clone_flags & CLONE_VFORK) && (retval > 0)) down(&sem); //這裏就是達到扣留一個進程的目的
return retval; } ************************************************************************ 從上面的代碼能夠看出,clone的工做相比於fork少了地址空間、文件句柄、信號量等的拷貝,也就是線程的地址空間、文件句柄、信號量等是共享父進程的,這也是gemfield本文最開始處的背景。
下面用2個簡單的程序來演示下:
第一個:fork.c,建立進程
***********************************************
#include <stdio.h> #include <unistd.h> int main(int argc,char **argv) { int gemfield=3; int ret = fork(); printf(「gemfield do fork…\n」); scanf(「%d」,gemfield); } ************************************************ gcc fork.c -o fork ./fork & ps -ef xH 輸出:
UID PID PPID C STIME TTY STAT TIME CMD …… gemfield 21776 21775 0 14:20 pts/2 Ss 0:00 -bash gemfield 22015 21533 0 14:42 pts/1 S+ 0:00 ./fork gemfield 22016 22015 0 14:42 pts/1 S+ 0:00 ./fork gemfield 22018 21776 0 14:42 pts/2 R+ 0:00 ps -ef xH ……
第二個:clone.c ,建立線程 **************************************************** #include <stdio.h> #include <pthread.h> int gemfield =0; void civilnet_cn(){ printf(「gemfield do clone***\n」); scanf(「%d」,gemfield); }
int main(int argc,char **argv) { pthread_t tid; int ret = pthread_create(&tid,NULL,civilnet_cn,NULL); printf(「gemfield do clone…\n」); scanf(「%d」,gemfield); } **************************************************** gcc clone.c -lpthread -o clone ./clone& ps -ef xH 輸出:
UID PID PPID C STIME TTY STAT TIME CMD …… gemfield 21533 21530 0 12:10 pts/1 Ss 0:00 -bash gemfield 21952 21533 0 14:30 pts/1 Sl+ 0:00 ./clone gemfield 21952 21533 0 14:30 pts/1 Sl+ 0:00 ./clone gemfield 21984 21776 0 14:41 pts/2 R+ 0:00 ps -ef xH ……
看出區別了嗎?兩個線程的父進程都是bash,但本身的pid是同樣的。
最後用一個比喻來總結下: 一、一個進程就比如一個房子裏有一我的; 二、clone建立線程就至關於在這個房子裏添加一我的; 三、fork建立進程就至關於再造一個房子,而後在新房子裏添加一我的;
有了上面的比喻後,咱們就清楚不少了: 一、線程之間有不少資源能夠共享:好比廚房資源、洗手間資源、熱水器資源等; 二、而對於進程來講,一個概念就是進程間通訊(你要和另一個房子裏的人通訊要比一個房子裏的兩我的之間通訊複雜); 三、線程之間由於共享內存,因此經過一個全局的變量就能夠交換數據了; 四、但與此同時,對於線程來講,又有新的概念產生了:一我的使用洗手間的時候,得鎖上以防止另外一我的對洗手間的訪問;一我的(或幾我的)睡覺的時候,另一我的能夠按照以前約定的方式來叫醒他;熱水器的電源要一直開着,直到想洗澡的人數減爲0;
上面的概念,在gemfield的後文中術語化的時候,你就不會再以爲很深奧或者枯燥了。