Linux系统分析——从进程创建到可执行文件执行
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/文章目录task_struct数据结构fork()函数这次主要谈谈可执行文件的执行以及其在内核运行的内部机制。内容一半是理论,一半是coding。 task_struct数据结构taskstructtask_structtaskstruct结构就是为进程描述符(p...
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
代码段的说明请直接在代码段内的注释查看, 不另解释.
基于linux-3.18.6, 和linux-5.0源码有些出入.
这次主要谈谈可执行文件的执行以及其在内核运行的内部机制。
内容一半是理论,一半是coding。
PS:博客是为了交代任务匆忙间写下的,关于编译链接和ELF内容没有太多描述,仅仅列出了概念。对fork()部分的代码,现在只能找到Linux-5.0内核的代码,和Linux-3.18的工作原理相似,但改动较多,在有限时间内看的也是一知半解。关于schedule()部分的工作机制则可以放心食用。
task_struct数据结构
t
a
s
k
_
s
t
r
u
c
t
task\_struct
task_struct结构就是为进程描述符(process descriptor)工作的。进程描述符的作用就是使内核对每个进程所作的事情进行清楚的描述。比如,内核必须知道进程的优先级,它是正在CPU上运行还是因为某件事被阻塞,给他分配了什么样的地址空间等等。进程描述符太过复杂,我们用一张图来大致示意一下:
fork()函数
谈fork()函数之前,需要先了解clone()函数,clone()是在C语言库中定义的一个封装函数,他负责建立新轻量级进程的堆栈并且调用对程序猿隐藏的**clone()**系统调用。而传统的fork()系统调用在Linux中是用clone()实现的,其中clone()的flags参数(flags参数是各种各样的信息。低字节指定子进程结束时发送到父进程的信号代码,通常选择SIGCHLD信号,其他字节另有用处,不细说)指定为SIGCHLD及所有清零的clone标志,而他的child_stack(child_stack表示把用户态堆栈指针赋值给子进程esp寄存器)当前的堆栈指针,当父子进程中有一个要识图改变堆栈,就立即得到各自用户态堆栈的copy。
do_fork()函数
先看一段linux-5.0的源代码:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
return _do_fork(clone_flags, stack_start, stack_size,
parent_tidptr, child_tidptr, 0);
}
可以看到do_fork()函数的主题内容是另外写得一小段函数_do_fork其参数是一样的, 为什么会画蛇添足增加这么一段呢?我们看看源码中在do_fork()下面的一个函数pid_t kernel_thread():
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0);
}
其实重点在于对do_fork()中某些参数的理解:
- regs:指向通用寄存器值的指针
- stack_size: 这个参数在do_fork中未被使用
- parent_tidptr,child_tidptr:这和clone()中的对应参数ptid和ctid相同.
其实不同的函数, 其对更底层的_do_fork()函数的使用需求是不一样的, 故添加了一个新的_do_fork进行泛化. 接下来看_do_fork():
这里我们只讨论do_fork()调用_do_fork()的情况, 也就是_do_fork()中的参数全为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,
unsigned long tls)
{
struct completion vfork;
struct pid *pid;
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
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;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
return nr;
}
代码中的步骤如下:
- 检查父进程ptrace字段
- 调用copy_process()复制进程描述符.该函数待会分析.
实验步骤
首先模拟系统:
分别设置断点:
b sys_clone
b do_fork
b copy_process
编译链接和ELF
编译概念
广义的代码编译过程,实际上应该细分为:预处理,编译,汇编,链接。
预处理过程,负责头文件展开,宏替换,条件编译的选择,删除注释等工作。gcc –E表示进行预处理,生成.i文件。
编译过程,负载将预处理生成的文件,经过词法分析,语法分析,语义分析及优化后生成汇编文件。gcc –S表示进行编译,将预处理后的文件不转换成汇编语言,生成文件.s。
汇编,是将汇编代码转换为机器可执行指令的过程。通过使用gcc –C或者as命令完成,生成.o目标文件。
链接,负载根据目标文件及所需的库文件产生最终的可执行文件。链接主要解决了模块间的相互引用的问题,分为地址和空间分配,符号解析和重定位几个步骤。
图示如下:
gcc常用参数
-
-c
只激活预处理,编译,和汇编,也就是他只把程序做成obj文件
例子用法:
gcc -c hello.c -
-S
只激活预处理和编译,就是指把文件编译成为汇编代码。
例子用法
gcc -S hello.c -
-E
只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面.
例子用法:
gcc -E hello.c > pianoapan.txt
gcc -E hello.c | more -
-o
制定目标名称,缺省的时候,gcc 编译出来的文件是a.out
例子用法
gcc -o hello hello.c
静态库与动态库
静态库:这类库的名字一般是libxxx.a。利用静态函数库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。可理解为一堆.o文件的打包。
动态库:静态库*.a文件的存在主要是为了支持较老的a.out格式的可执行文件而存在的。目前用的最多的要数动态库了。这类库的名字一般是libxxx.M.N.so,相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。linux系统有几个重要的目录存放相应的函数库,如/lib /usr/lib。
ELF
elf文件是一种目标文件格式,用于定义不同类型目标文件以什么样的格式,都放了些什么东西。主要 用于linux平台。windows下是PE/COFF格式。
可执行文件、可重定位文件(.o)、共享目标文件(.so)、核心转储文件都是以elf文件格式存储的。
ELF文件组成部分:文件头、段表(section)、程序头。
实验部分
分别设置断点进行跟踪:
b sys_execve
b load_elf_binary
b start_thread
跟踪断点后对各断点内部函数查看:
Schedule()函数
schedule函数是进程之间进行切换的关键函数,这一函数在我上一次实验已经有过一段分析,但是那时还没有对系统进程的切换有较好的掌握,只是粗略的意识到链表,任务(task)是怎样切换的,如果没人告诉我这就是进程切换,恐怕我是意识不到了。
本次实验则是在学习过进程描述符(process descriptor),线程描述符(thread_info)以及内核堆栈等等之后才做的,对schedule的流程较上一次来说认识更清楚些,可以稍微描述schedule的机制了。
进程切换主要分为两部分:
- 切换页全局目录以安装一个新的地址空间
- 切换内核堆栈和硬件上下文
进程切换的第二步由switch_to宏执行。代码和解释写在下面:
/*
* schedule() is the main scheduler function.
*/
asmlinkage void __sched schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable(); //禁止内核抢占
cpu = smp_processor_id(); //获取当前CPU
rq = cpu_rq(cpu); //获取该CPU维护的运行队列(run queue)
rcu_note_context_switch(cpu); //更新全局状态,标识当前CPU发生上下文的切换。
prev = rq->curr; //运行队列中的curr指针赋予prev。
schedule_debug(prev);
if (sched_feat(HRTICK))
hrtick_clear(rq);
raw_spin_lock_irq(&rq->lock); //锁住该队列
switch_count = &prev->nivcsw; //记录当前进程的切换次数
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { //是否同时满足以下条件:1该进程处于停止状态,2该进程没有在内核态被抢占。
if (unlikely(signal_pending_state(prev->state, prev))) { //若不是非挂起信号,则将该进程状态设置成TASK_RUNNING
prev->state = TASK_RUNNING;
} else { //若为非挂起信号则将其从队列中移出
/*
* If a worker is going to sleep, notify and
* ask workqueue whether it wants to wake up a
* task to maintain concurrency. If so, wake
* up the task.
*/
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
deactivate_task(rq, prev, DEQUEUE_SLEEP); //从运行队列中移出
/*
* If we are going to sleep and we have plugged IO queued, make
* sure to submit it to avoid deadlocks.
*/
if (blk_needs_flush_plug(prev)) {
raw_spin_unlock(&rq->lock);
blk_schedule_flush_plug(prev);
raw_spin_lock(&rq->lock);
}
}
switch_count = &prev->nvcsw; //切换次数记录
}
pre_schedule(rq, prev);
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
put_prev_task(rq, prev);
next = pick_next_task(rq); //挑选一个优先级最高的任务将其排进队列。
clear_tsk_need_resched(prev); //清除pre的TIF_NEED_RESCHED标志。
rq->skip_clock_update = 0;
if (likely(prev != next)) { //如果prev和next非同一个进程
rq->nr_switches++; //队列切换次数更新
rq->curr = next;
++*switch_count; //进程切换次数更新
context_switch(rq, prev, next); /* unlocks the rq */ //进程之间上下文切换
/*
* The context switch have flipped the stack from under us
* and restored the local variables which were saved when
* this task called schedule() in the past. prev == current
* is still correct, but it can be moved to another cpu/rq.
*/
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else //如果prev和next为同一进程,则不进行进程切换。
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
preempt_enable_no_resched();
if (need_resched()) //如果该进程被其他进程设置了TIF_NEED_RESCHED标志,则函数重新执行进行调度
goto need_resched;
}
实验内容
schedule在系统创建进程的过程中肯定是不断调用的,这里仅仅展示一步。
更多推荐
所有评论(0)