原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
一、实验步骤及分析
进程描述
我们通过进程控制块来描述来描述进程,又称其为进程描述符,他提供了进程相关的所有信息,例如状态、进程双向链表管理、控制台、文件系统、内存管理、进程间通信等等。
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
…………
#if defined(CONFIG_BCACHE) || defined(CONFIG_BCACHE_MODULE)
unsigned int sequential_io;
unsigned int sequential_io_avg;
#endif
};
进程状态转换图
使用fork进程创建子进程后,新进程的状态是TASK_RUNNING,将此进程从就绪队列调度执行时状态也是TASK_RUNNING。
进程创建
0号进程初始化是通过硬编码固定下来的,init_task为0号进程的进程描述符的结构体变量。init_task初始化如下:
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);
rest_init调用kernel_thread创建1号进程和2号进程,一个是kernel_init,init用户进程;另一个是kthreadd进程,是所有内核进程的祖先,管理内核进程。
static noinline void __init_refok rest_init(void){
....
kernel_thread(kernel_init,NULL,CLONE_FILES);
....
pid = kernel_thread(kthreadd,NULL,CLONE_FS|CLONE_FILES);
....
}
使用fork函数创建进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
int pid;
pid = fork();
if(pid < 0)
{
fprintf("stderr","failed!");
}
else if(pid == 0)
{
printf("This is child process!\n");
}
else
{
printf("This is parent process!\n");
wait(NULL);
printf("child complete!\n");
}
return 0;
}
fork系统调用把当前进程又复制了一个子进程,此时两个进程执行相同的代码,只是父进程和子进程的返回值不同,而两个进程输出信息在一个终端。
fork系统调用
fork系统调用时,用户态用int $0x80指令触发中断机制,cpu自动从用户态堆栈转为内核态堆栈,将ss:esp,cs:eip以及eflags压到当前进程内核堆栈中,接下来执行system_call,其用于保存现场,调用系统调用内核函数,处理完后返回,恢复现场。最后iret将ss:esp,cs:eip以及eflags从内核堆栈恢复到相应寄存器中。
fork,vfork以及clone 、kernel_thread函数都是通过do_fork来创建一个进程,参考代码如:
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);
}
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
#endif
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
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)
{
struct task_struct *p;
int trace = 0;
long nr; // //子进程id
/*
* 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); // //创建子进程描述符
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID); // 获得task结构体的pid
nr = pid_vnr(pid); // 根据pid结构体获得进程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);
} else {
nr = PTR_ERR(p);
}
return nr;
}
do_fork函数主要完成了以下工作:
- 通过调用copy_process()复制父进程信息,创建描述符及其他的数据结构
- 获得pid
- 调用wake_up_new_task(p)将子进程加入调度器队列,等待cpu资源运行
copy_process函数
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;
…………
retval = security_task_create(clone_flags);//安全性检查
…………
p = dup_task_struct(current); // 复制pcb
…………
//检查用户进程数是否超过限制
retval = -EAGAIN;
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
…………
//检查进程数量是否超过max_threads
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
retval = sched_fork(clone_flags, p);
if (retval)
goto bad_fork_cleanup_policy;
retval = perf_event_init_task(p);
if (retval)
goto bad_fork_cleanup_policy;
retval = audit_alloc(p);
if (retval)
goto bad_fork_cleanup_perf;
/* copy all the process information */
shm_init_task(p);
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_files(clone_flags, p);
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
retval = copy_thread(clone_flags, stack_start, stack_size, p);
…………
return p;//返回被创建的子进程描述符指针
…………
}
copy_process函数主要完成以下工作:
- 调用dup_task_struct复制父进程描述符
- 调用copy_thread初始化子进程内核栈
- 将子进程置为就绪态
- 采用写时复制技术复制其他进程资源
- 设置子进程pid
dup_task_struct函数
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int node = tsk_fork_get_node(orig);
int err;
tsk = alloc_task_struct_node(node);
if (!tsk)
return NULL;
ti = alloc_thread_info_node(tsk, node);
if (!ti)
goto free_tsk;
err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_ti;
tsk->stack = ti;
#ifdef CONFIG_SECCOMP
/*
* We must handle setting up seccomp filters once we're under
* the sighand lock in case orig has changed between now and
* then. Until then, filter must be NULL to avoid messing up
* the usage counts on the error path calling free_task.
*/
tsk->seccomp.filter = NULL;
#endif
setup_thread_stack(tsk, orig);
clear_user_return_notifier(tsk);
clear_tsk_need_resched(tsk);
set_task_stack_end_magic(tsk);
#ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = get_random_int();
#endif
/*
* One for us, one for whoever does the "release_task()" (usually
* parent)
*/
atomic_set(&tsk->usage, 2);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
tsk->task_frag.page = NULL;
account_kernel_stack(ti, 1);
return tsk;
free_ti:
free_thread_info(ti);
free_tsk:
free_task_struct(tsk);
return NULL;
}
dup_task_struct函数创建两个页,一部分存放thread_info,另一部分存放内核堆栈,复制父进程task_struct ,thread_info结构,然后将task指针指向子进程的进程描述符。
copy_thread函数
int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;
p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
if (unlikely(p->flags & PF_KTHREAD)) {//内核线程
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs));
//内核进程,从ret_from_kernel_thread开始执行
p->thread.ip = (unsigned long) ret_from_kernel_thread;
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl();
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr = NULL;
return 0;
}
//复制内核堆栈(复制父进程寄存器信息)
*childregs = *current_pt_regs();
childregs->ax = 0;//子进程eax =0,返回值为0
if (sp)
childregs->sp = sp;
//从ret_from_fork执行
p->thread.ip = (unsigned long) ret_from_fork;
task_user_gs(p) = get_user_gs(current_pt_regs());
p->thread.io_bitmap_ptr = NULL;
tsk = current;
err = -ENOMEM;
if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = 0;
return -ENOMEM;
}
set_tsk_thread_flag(p, TIF_IO_BITMAP);
}
err = 0;
/*
* Set a new TLS for the child thread?
*/
if (clone_flags & CLONE_SETTLS)
err = do_set_thread_area(p, -1,
(struct user_desc __user *)childregs->si, 0);
if (err && p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max = 0;
}
return err;
}
copy_thread函数是对内核信息的初始化。子进程开始执行的起点分为两种情况,内核进程从ret_from_kernel_thread开始执行,用户态进程从ret_from_fork执行。
使用gdb调试fork
使用以下指令启动menuOS
cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
make rootfs
cd LinuxKernel
gdb
file linux-3.18.6/vmlinux
target remote:1234
分别在sys_clone、do_fork、dup_task_struct、copy_process、copy_thread、ret_from_fork出设置断点。
编程使用exec*库函数加载一个可执行文件
编辑文件myexec.c,生成预处理文件myexec.cpp,编译成汇编代码myexec.s,编译成目标代码,即二进制文件myexec.o,链接成可执行文件myexec,运行./myexec。
编译链接的过程和ELF可执行文件格式
ELF可执行文件格式
可重定位文件:一般是中间文件,需要和其他文件一起来创建可执行文件、静态库文件、共享目标文件。
可执行文件:文件中保存着一个用来执行的文件。
共享目标文件:指可以被可执行文件或其他库文件使用的目标。
execve函数过程描述
整体调用关系为 execve->sys_execve->do_execve() -> do_execve_common()->exec_binprm()->search_binary_handler()->load_elf_binary()->start_thread()。
大致处理过程如下:
sys_execve中的do_execve() 读取128个字节的文件头部,判断可执行文件的类型
调用search_binary_handler()搜索和匹配合适的可执行文件装载处理过程。
ELF文件由load_elf_binary()函数负责装载,load_elf_binary()函数调用了start_thread函数,创建新进程的堆栈。
使用gdb跟踪分析一个execve系统调用内核处理函数do_execve
分别在sys_execve、do_execve() 、do_execve_common()、exec_binprm()、search_binary_handler()、load_elf_binary()、start_thread()设置断点。
新的可执行程序执行起点以及为什么execve系统调用返回后新的可执行程序能顺利执行?
新的可执行程序开始执行的起点在于修改的调用 execve系统调用时压入内核堆栈的EIP的值,此时标志着当前进程可执行文件已经完全替换成新的可执行文件。
对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?
静态链接:elf_entry指向可执行文件的头部,一般是main函数,是新程序执行的起点,一般地址为0x8048XXX的位置。
动态链接:elf_entry指向ld即动态链接器的起点load_elf_interp。
使用gdb调试一个schedule()函数
分别在schedule,pick_next_task,context_switch,__switch_to设置断点
可以看出 schedule调用_schedule,_schedule调用pick_next_task,context_switch函数,context_switch函数调用__switch_to。pick_next_task函数是根据调度策略和调度算法选择下一进程,context_switch函数负责进程的切换。
分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系
首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。然后将prev的内核堆栈指针esp存入prev->thread.esp中。
把将next进程的内核栈指针next->thread.esp置入esp寄存器中,将当前进程的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度,通过jmp指令转入一个函数__switch_to,__switch_to中jmp与return的匹配,return 会弹出返回地址,因为jmp不会压栈,return弹出的则是栈顶地址即$1f标识之处。恢复next上次被调离时推进堆栈的内容。next进程开始执行。
进程上下文及与中断上下文切换的关系
进程上下文切换需要保存切换进程的相关信息(thread.sp和thread.ip);中断上下文的切换是在一个进程的用户态到一个进程的内核态,或从进程的内核态到用户态,切换进程需要在不同的进程间切换,但一般进程上下文切换是套在中断上下文切换中的。例如,系统调用作为中断陷入内核,,调用schedule函数发生进程上下文切换,系统调用返回,完成中断上下文的切换。
总结
通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行,所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行.同理,硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。Linux内核工作在进程上下文或者中断上下文。提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文;另一方面,中断处理程序,异步运行在中断上下文。中断上下文和特定进程无关。