以前写的东西,感觉写的过于繁琐,从这以后尽量写的简洁明朗一点,也有助于我自己以后再看。(内核版本linux-3.2.36)
我们要解决的问题。
1. 简单描述内核线程创建过程。
2. 为什么kthread_create()调用后,我们还要调用wake_up_process来唤醒调用我们的线程函数,用户态并不需要。
kthread_create(threadfn, data, namefmt,arg...);
这是创建内核进程的主要函数。
它会唤醒kthread线程去建立线程,然后通过一个完成量等待完成,如果创建成功会设置调度方法(默认用nomal)和优先级(默认为0)。
kthread最终会调用kernel_thread,其实你可以用kernel_thread直接创建线程,可以上网看看如何使用,也很简单。
kernel_thread是依赖处理器架构的,但是最终都是调用do_fork(),我看了x86和arm都是一样的调用参数
do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0,®s, 0, NULL, NULL);
所谓的依赖处理机主要体现在regs,regs在arm中是什么,我们下面再看。先记住它。
下面看do_fork,网上有do_fork的分析,但是和我这个版本好像都不一样。
我会省略一些代码。只做说明
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
在开始分配东西之前,做一些初步的论证和权限检查
确定那个事件报告给tracer。但下面的条件要成立。
(likely(user_mode(regs)) && !(clone_flags & CLONE_UNTRACED))
条件的意思是用户模式且没有CLONE_UNTRACED,即不是通过kernel_thread创建。
在arm上,user_mode为(((regs)->ARM_cpsr & 0xf) ==0) 看看arm的cpsr你就知道了,用户模式判断
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL,trace);
下面会判断是否有效,如果有效p,向下
然后分配PID。
根据CLONE_PARENT_SETTID,新创建的子进程的ID号登记到父进程的相关字段中。
根据CLONE_VFORK初始化完成量,这个完成量在mm_release时向父进程发送信号,唤醒父进程。
audit_context初始化,其实和父进程的一样,这是审计上下文,
/*
每个进程的进程结构(或者进程上下文)含有审计上下文结构audit_context指针,审计上下文记录进程上下文的审计信息,当进程从进入系统调用和 退出系统调用时,使用审计上下文结构audit_context记录系统调用进入和退出的各种属性数据,如:时间戳、参数、调用号等。审计上下文还通过辅 助数据结构链表记录进程运行中的各种关键数据结构的审计信息。详细在此http://book.51cto.com/art/200712/62881.htm
*/
为了防止在creation时被追踪,我们设置了PF_STARTING标准,现在清除它。
调用wake_up_new_task():唤醒一个新的task。这个函数主要是将新的进程加入到进程调度队列并设此进程为可被调度的,以后这个进程可以被进程调度模块调度执行。
但是现在执行的不是我们的线程函数,我们的线程还要等到调用wake_up_process()再运行。
下面会解释。
根据tace标志,告知ptracer
根据CLONE_VFORK,让父进程freezer。
}
现在我们还有p =copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL, trace);
这个函数也有好多网上的分析,我分析我关心的,其他的自己看看:
http://hi.baidu.com/zengzhaonong/item/df005e13633205a4feded523
我说一些:
调用p = dup_task_struct(current);
这个为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。
还有一个copy_mm(clone_flags,p);
这个函数在这我们先记住就可以了,因为设置了CLONE_VM,这个函数只是把父进程的mm和mm_active赋值给子进程。内核线程是没有用户空间的mm。mm为空。即使是用户线程,在这也是空,因为写时拷贝机制。
下面还有一句
retval = copy_thread(clone_flags, stack_start,stack_size, p, regs);
这个要对应于kernel_thread。上面说了这个基于平台。我会以arm平台作为讲解。没有兴趣的就不要往下看了
copy_thread是使用参数regs初始化子进程的内核堆栈空间
我们先看kernel_thread
调用时pid =kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
pid_t kernel_thread(int (*fn)(void *), void*arg, unsigned long flags)
{
struct pt_regs regs;
memset(®s, 0, sizeof(regs));
regs.ARM_r4 = (unsigned long)arg; //这个arg是一个结构体,里面记录我们的线程函数,传入参数,还有创建线程时要等待的完成量
regs.ARM_r5 = (unsigned long)fn;//这个是kthread,内核提供的,先记住它。
regs.ARM_r6 = (unsignedlong)kernel_thread_exit;//我的就是do_exit
regs.ARM_r7 = SVC_MODE | PSR_ENDSTATE | PSR_ISETSTATE;//看下面
regs.ARM_pc = (unsigned long)kernel_thread_helper;
regs.ARM_cpsr = regs.ARM_r7 | PSR_I_BIT;//结合上面就是禁止IRQ,系统模式。有个E位,我的arm920t没有这个位。
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, ®s, 0, NULL,NULL);
}
kernel_thread_helper。我们看看它干了什么。
asm( ".pushsection .text\n"//这是elf段堆栈操作命令,它将当前段(及子段)推入段堆栈的顶部。
" .align\n"//对齐
" .type kernel_thread_helper,#function\n"//函数符号
"kernel_thread_helper:\n"
#ifdef CONFIG_TRACE_IRQFLAGS//这个不看
" bl trace_hardirqs_on\n"
#endif
" msr cpsr_c, r7\n"//设置cpsr_c,这个和cpsr区别是屏蔽0~7位。即只保留N Z C V位
" mov r0, r4\n"//传入函数的参数
" mov lr, r6\n"//退出时的函数。
" mov pc, r5\n"//执行r5函数
" .size kernel_thread_helper, . -kernel_thread_helper\n"//设置内存大小,为0
" .popsection");//堆栈顶段出栈
从上面看kernel_thread_helper会执行我们自定义的线程函数。
它是如何调用,这个问题应该是调度的问题。不过还是简单说一下
我们看看copy_thread,就看几句
struct thread_info *thread = task_thread_info(p);
struct pt_regs *childregs = task_pt_regs(p);
*childregs = *regs;
childregs->ARM_r0 = 0;
childregs->ARM_sp = stack_start;//传入的栈地址
从上面看除了栈地址不一样,其他都一样。
memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));
thread->cpu_context.sp = (unsigned long)childregs;//让这个进程的thread_info的sp指向这个regs,这个就是上面kernel_thread赋值的。
thread->cpu_context.pc = (unsigned long)ret_from_fork;
…
if (clone_flags & CLONE_SETTLS)
thread->tp_value =regs->ARM_r3;
CLONE_SETTLS是子进程创建新的TLS(thread-local storage);
上面的structthread_info:每个任务都有一个thread_info结构,放在内核栈的尾端。
看到上面的cpu_context.pc了吧,它是进程下一次调度时的指令开始地址。所以下一次会执行ret_form_fork,不细看了,它会从堆栈中找到kernel_thread_helper地址并执行。
现在让我们弄清楚为什么内核线程要调用wake_up_process后再能运行。为什么上面已经调用wake_up_new_task(),但是没有运行我们的线程函数。在调度时运行的是kthread。
static int kthread(void *_create)
{
/* Copy data: it's on kthread's stack */
struct kthread_create_info *create = _create;
int (*threadfn)(void *data) = create->threadfn;//我们的模块线程函数
void *data = create->data;//传入参数
struct kthread self;
int ret;
self.should_stop = 0;
self.data = data;
init_completion(&self.exited);
current->vfork_done = &self.exited;
/* OK, tell user we're spawned, wait for stop or wakeup */
__set_current_state(TASK_UNINTERRUPTIBLE);//在这设置为睡眠,所以你要通过调用wake_up_process来唤醒。
create->result = current;
complete(&create->done);//这个会通知上面的kthread_create(),没有它就会死机了。
schedule();//再次调度,当然不会掉当前的函数。因为被设置为TASK_UNINTERRUPTIBLE
ret = -EINTR;
if (!self.should_stop)
ret = threadfn(data);//等到什么时候可以运行了,才会到这。
/* we can't just return, we must preserve "self" on stack */
do_exit(ret);
}