我看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、要不止一遍的看,多看几遍,没事就拿出来看看。这样就能多理解一点。
最后贴一句正在听的歌的歌词,是你提醒我,别怕去幻想,向往内心躲避惯的渴望。
晚安。祝好。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值