1. 程式人生 > >我看task_struct結構體和do_fork函式

我看task_struct結構體和do_fork函式

先來看看task_struct結構體。
眾所周知,task_struct結構體是用來描述程序的結構體,程序需要記錄的資訊都在其中,下面我們來看看其中的具體專案。結構體儲存在linux/sched.h中。
具體的欄位有

volatile long state; 
void *stack;
...
struct task_struct __rcu *real_parent;
struct task_struct __rcu *parent;
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;

意思分別是
程序的狀態:可執行,不可執行和
棧頭
真正的父程序
養父程序
子程序的連結串列
兄弟程序連結串列
執行緒組長
下面來看看do_fork()函式的大概過程。
我看的核心版本是3.14.54,核心版本不一樣,程式碼的差別很大的,但是重要的機制都沒什麼變化。
do_fork函式頭如下所示:

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,                           
          unsigned long stack_size,
          int
__user *parent_tidptr, int __user *child_tidptr)

clone_flag 是clone程序的標誌引數(這是 一個很重要的引數,如果我遇到了一些函式在判斷clone_flags的if裡邊呼叫的,理解不了的話可以去看看這個flag的定義,定義會解釋這個flag是為了標記什麼,良心出品,幫助理解的利器),clone_flags定義了很多,在這裡我就不贅述了。
stack_start 是棧的起始地址
stack_size 棧的大小(初值被設定為0)
parent_tidptr 初值被設定為NULL
child_tidptr 初值被設定為NULL

if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)     
            trace = PTRACE_EVENT_VFORK;
        else if ((clone_flags & CSIGNAL) != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace=PTRACE_EVENT_FORK;                                                                    

        if (likely(!ptrace_event_enabled(current, trace)))                                                
            trace = 0;
    }

先是判斷標誌位的內容根據這些標誌位來設定tracce標誌(trace是程序是不是可追蹤的標誌,這一塊的邏輯關係理不太清楚),下來緊接著就呼叫了copy_process函式(這個函式是一個很重要的函式,基本上完成了do_fork的工作),

 p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);    

呼叫函式copy_process,用p來接收返回值,所以copy_process函式返回值型別是task_struct型別,並且p用來儲存新程序的pid。對函式copy_process的分析放在後邊,我們接著來看do_fork函式的下邊部分。

if (!IS_ERR(p)) {
...
}
else {
        nr = PTR_ERR(p);
     }
     return nr;

copy_process函式呼叫成功了則執行if函式裡邊的步驟,否則執行函式PTR_ERR初始化nr的值,程式執行結束。這裡根據man手冊我們可以知道PTR_ERR是給nr賦值為-1,if中裡邊的程式碼如下:

...
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
...
wake_up_new_task(p);
...
return nr;

獲取新程序的struct pid結構體(新版本把之前的pid和其他的一些資訊包裝成了結構體,不用太糾結於這個,而後邊緊跟的pid_vnr函式就是獲取引數pid(pid結構體)中的pid欄位(程序號)),通過這個結構體給新程序分配一個pid(程序號),喚醒新程序,根據不同的主調函式進行一些不同的操作,返回nr。
do_fork函式主要的建立操作是copy_process函式來執行的,現在我們來具體分析一下這個函式。函式的引數如下所示。

(unsigned long clone_flags,
 unsigned long stack_start,     
 unsigned long stack_size,      
 int __user *child_tidptr,      
 struct pid *pid,               
 int trace)         

前四個引數是直接把do_fork函式裡邊的引數傳過來,第四個引數是一個pid結構體(獲取程序pid的時候會用到),最後一個是可追蹤標誌。

if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))                                 
        return ERR_PTR(-EINVAL);   

    if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
        return ERR_PTR(-EINVAL);  
      if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);

這都是一些判斷clone_flags是不是合法的,申請新的名稱空間和clone父親的fs必須選擇一個,如果建立的是執行緒的話必須共享父親的訊號處理函式等等,這一部分是很好理解的,在此不再贅述,我們來看接下來的部分。

retval = security_task_create(clone_flags);
p = dup_task_struct(current);
ftrace_graph_init_task(p);
rt_mutex_init_task(p);
retval = copy_creds(p, clone_flags);
...
delayacct_tsk_init(p);
copy_flags(clone_flags, p);
INIT_LIST_HEAD(&p->children);  
INIT_LIST_HEAD(&p->sibling);   
    rcu_copy_process(p);                                      p->vfork_done = NULL;     
spin_lock_init(&p->alloc_lock);
init_sigpending(&p->pending);

建立一個安全程序(不是很重要的一個函式,第二遍看依舊沒懂是什麼),然後是複製當前程序的task_struct結構體給p(函式具體介紹後邊再說,先繼續來看copy_peocess函式),子程序定義自己的函式棧,掛在新程序的task_struct的ret_stack域(是什麼沒搞懂)裡邊。(存放被調函式的一些資訊)確保不使用父程序的什麼什麼棧,然後初始化這個棧。初始化程序互斥鎖,拷貝父程序的安全證書,進行一些判斷,允許子程序延遲,設定NOEXEC標誌,初始化子程序的孩子連結串列和兄弟連結串列,初始化rcu鎖,初始化自旋鎖,初始化順序鎖,清空從父程序繼承來的未決訊號連結串列。初始化一些時間之類的資訊,到這裡產生新程序所需要的環境已經全部都初始化完成了,之後開始進入排程部分。程式碼如下:

retval = sched_fork(clone_flags, p);
retval = perf_event_init_task(p);
retval = copy_files(clone_flags, p);
retval = copy_fs(clone_flags, p);
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
...
INIT_LIST_HEAD(&p->thread_group);
p->task_works = NULL; 
...
p->real_parent = current;

設定新程序狀態為就緒狀態以及一些別的排程時候需要的變數,初始化新程序的優先順序以及一些上下文資訊。拷貝父程序的files,fs,signal已經signal等一些資料。初始化程序的tid,後邊就是根據clone_flags(vfork函式,fork函式和clone函式在這裡執行有一些區別)初始化新進成task_struct的一些欄位,初始化執行緒組連結串列,清空task_works,建立父子程序關係。最後做了一些處理執行緒組的工作和把新程序加入其父程序的還連連結串列中等一些工作。到這裡do_fork函式就執行完了,新進成已經建立成功並且插入就緒佇列裡邊了。
在上邊我們看到了子程序複製父程序的task_struct結構體是在函式dup_task_struct中完成的,

p = dup_task_struct(current);

這是函式dup_task_struct的呼叫形式,在這裡我們將一個很有意思的巨集——current巨集,根據名稱可以知道這個巨集定義是獲得當前正在執行的程序的task_struct,我們可以調出這個巨集的巨集定義,以及巨集定義函式的實現(下邊的current和函式get_current()不一定在一個檔案中定義的我,我為了方便,把他們寫在了一起)

#define current     get_current()
#define get_current()   (current_thread_info()->task)
...
struct thread_info *current_thread_info(void)                                                             
{
    struct thread_info *ti;
    asm volatile (" clr   .s2 B15,0,%1,%0\n"
              : "=b" (ti)
              : "Iu5" (THREAD_SHIFT - 1));
    return ti;
}

根據上邊的定義可以得到實現功能的函式數current_thread_info函式,
這個函式沒有引數,返回值型別是thread_info的指標型別,即為指向當前執行程序的thread_info的指標。
先來提一點理論上的東西,在linux系統中thread_info和核心棧共用8k空間,而且thread_info中有一個指標指向了task_struct結構體(task域),所以函式current_thread_info裡邊這一行內聯彙編執行的內容是根據當前程序的esp暫存器的值得到thread_info的起始地址(thread_info在stack的低地址部分,核心棧在高地址部分),然後進而得到task_struct的地址。
接下來我們來看看dup_task_struct函式的內容(這時一個很重要的結構體實現比較簡單易懂)函式的定義程式碼如下所示:

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
...
int node = tsk_fork_get_node(orig);
tsk = alloc_task_struct_node(node);
if (!tsk)                                              
     return NULL;       

    ti = alloc_thread_info_node(tsk, node);
    if (!ti)
        goto free_tsk;        

函式返回值是task_struct指標型別,引數是父程序的task_struct,函式功能是獲取將被建立程序的task_struct資訊。給定義的tsk(task_struct)和ti(thread_info)分配一塊記憶體(alloc_thread_info_node函式執行的操作是分配一塊記憶體(8096,THREAD_SIZE巨集),並返回這塊記憶體的虛地址),如果tsk分配空間失敗返回NULL,如果ti分配空間失敗,跳轉到free_tsk(釋放tsk分配好的空間)。如果分配成功接著執行下面的內容。

err=arch_dup_task_struct(tsk,orig);
    if (err)
        goto free_ti;
    tsk->stack = ti;
    setup_thread_stack(tsk, orig)       
    clear_user_return_notifier(tsk);

把父程序的task_struct賦給子程序,如果執行不成功,函式dup_task_struct返回NULL。如果成功返回0,接著執行下面的步驟。把剛剛分配好的ti這塊空間給了task_struct結構體的棧欄位用作子程序核心棧(這裡可以看出子程序複製了父程序的task_struct結構體全部內容,但是子程序有自己的棧空間)。函式setup_thread_stack實現的內容比較有意思,我們來看一下他的實現:

#define setup_thread_stack(p, org)          \                                                             
    *task_thread_info(p) = *task_thread_info(org);  \
    task_thread_info(p)->ac_stime = 0;      \
    task_thread_info(p)->ac_utime = 0;      \
    task_thread_info(p)->task = (p);

在這個函式的實現裡我們可以看到基本上都是在呼叫task_thread_info函式,先來根據函式名猜一下函式的功能:設定程序的thread_info內容。現在來看這個函式,程式碼如下:

#define task_thread_info(task)  ((struct thread_info *)(task)->stack) 

這是一個巨集定義的函式,這裡就比較繞我們;來一點一點理解。在前邊我們已經申請到了stack空間,但是在這個時候,stack裡邊是沒有資料的。函式task_thread_info得到引數task的stack。那麼函式setup_thread_stack是把父程序的task_struct中的stack賦給新程序(包括核心棧部分),然後讓thread_info的task只想子程序的task_struct。

到這裡第三遍基本上過完了,差不多能理解fork函式基本做了什麼事情,當然還有一些函式沒看懂。前三遍都是沒有去網上查閱資料自己根據之前的作業系統理論猜的。有自己理解的過程的確很重要。接下來打算跟著《深入理解linux核心》這本書再仔細過一遍。
談一點看核心基本感受:
1、去一個容易理解的函式名是一件很重要的事。
2、看的確實不容易,多看幾遍,要堅持看核心程式碼,看習慣了就好理解了。
3、看核心的時候要不求甚解,不要死扣每一個函式,挑重要的邊猜邊看,遇到看不明白的不要先去查閱資料,先跳過去看下一個,等看完第一遍了再回頭來,到時候醍醐灌頂。
4、要不止一遍的看,多看幾遍,沒事就拿出來看看。這樣就能多理解一點。
最後貼一句正在聽的歌的歌詞,是你提醒我,別怕去幻想,嚮往內心躲避慣的渴望。
晚安。祝好。