Linux 进程执行过程分析
Linux 进程执行过程分析
编号411,本文参考孟宁老师 Github 项目 https://github.com/mengning/linuxkernel
进程的基本要素
由于系统进程有一定的特殊性,这里主要分析普通用户进程。一般来讲,Linux 系统下的进程有几个基础要素:
-
可执行代码
可执行代码是进程的基本要素,这部分包含表示程序功能的进程私有代码和共享的链接库代码。
-
系统专用系统堆栈空间
进程专用的系统堆栈空间
-
进程控制块
即task_stuct数据结构,一方面,进程控制块包含的内容为内核调度提供了数据,另一方面,这个结构体记录了该进程的私有资源。
-
独立存储空间
独立的存储空间,即表示该进程拥有专有的用户空间(用户空间堆栈)。
除了以上的基本要素外,为了理解进程的执行过程,我们还需要理解以下基本内容:
- 进程的生命周期
一个进程被fork出来后,进入就绪态;当被调度到获得CPU执行时,进入执行态;如果时间片用完或被强占时,进入就绪态;资源得不到满足时,进入睡眠态(深度睡眠或浅度睡眠),比如一个网络程序,在等对方发包,此时不能占着CPU,进入睡眠态,当包发过来时,进程被唤醒,进入就绪态;如果被暂停,进入停止态;执行完成后,资源释放,此时父进程wait4还未收到它的信号,进入僵死态。即整个周期可能会涉及的状态有:就绪态,执行态,僵死态,停止态,睡眠态。
-
进程执行相关的系统调用
- fork() 调用
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
-
execve() 调用
- 预处理
首先在内核空间分配一个物理页面,然后调用do_getname()从用户空间拷贝文件名字符串。
- 预处理
-
调用主体函数do_execve()
-
我们既然要执行参数中给的二进制文件,首先需要打开文件,获取文件句柄file
-
然后我们需要一个linux_binprm结构体去保存函数具体的参数信息,包括文件名,argv,envp,还会将文件前128字节读到linux_binprm.buf中。
-
因为可执行文件的种类很多,比如elf,a.out等格式。我们需要从内核全局linux_binfmt队列中找到一个能够处理参数中所给的可执行文件的linux_binfmt结构,具体就是依次试用linux_binfmt结构中各自的load_binary()函数。
-
-
可执行文件的装载和投运(a.out为例)
-
与过去决裂,释放用户空间。
既然是要执行参数中给定的二进制文件,就需要放弃可能从父进程继承下来的用户空间,而使用本进程自己的用户空间。因此,需要检查是否与父进程通过指针共享用户空间,还是之前复制父进程用户空间。如果通过指针共享,说明本进程本身没有自己的用户空间,之前称为“进程”不合适,应该称作线程,就直接申请进程用户空间。如果复制父进程的用户空间,这是就需要全部释放。 -
装载可执行文件数据段代码段
这时可以将可执行文件装入进程的用户空间了,这时分两种情况:-
可执行文件不是"纯代码",需要通过do_brk()扩展数据段+代码段大小的空间,然后通过read()读取文件内容到用户空间
-
否则,如果文件系统提供mmap(),并且数据段和代码段长度与页面大小对齐,直接通过文件映射读取到用户空间,否则,通过1方法读取。
-
-
装载可执行文件堆栈段和bss段
用户空间堆栈区顶部当然是用户虚存空间顶部,即TASK_SIZE,为3GB,虚存地址为0xC000 0000的位置。
这里主要是设置用户堆栈区,包括envp[],argv[]以及argc
- start_thread()
-
代码实测
-
编写 fork() 和 execve() 的测试代码并进行测试
编写以下两个文件
- helloworld 文件
// file helloword.c #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { printf("Hello world!\n"); pid_t pid = getpid(); printf("pid of helloworld from helloword is %d\n", pid); return 0; } // end helloword.c
- start_process 文件
// file start_process.c #include<stdio.h> #include<unistd.h> #include<sys/types.h> int main() { pid_t pid; char *argv_execve[]={"helloworld", NULL}; char *envp[]={"PATH=/home/coolxxy/Code/ex03/", "USER=coolxxy", "STATUS=testing", NULL}; pid = getpid(); printf("pid of start_process is :%d\n",pid); printf("Starting systemcall execve......\n"); pid_t temp = fork(); if(temp == 0) { printf("i am here because i find fork() return 0, pid getted after fork() %d\n", getpid()); if(execve("./helloworld", argv_execve, envp) < 0) { perror("Error on execve"); } } else { printf("i am here because i find fork() return a posivate value %d, pid getted after fork() is %d\n", temp, getpid()); } return 0; } // end file start_process.c
分别编译以上两个文件并执行 start_process 文件,可以获得以下输出:
通过这个测试,可以发现 fork() 执行了两次返回,父进程中返回了子进程的 pid, 子进程中返回了0.
-
通过 gdb 调试分析 _do_fork() 过程调用
我分别给内核种的 _do_fork() 和 copy_process 函数添加断点并分析其调用过程
通过追踪这两个函数的执行过程,可以发现,_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;//在内存中分配一个 task_struct 数据结构,以代表即将产生的新进程
int trace = 0;
long nr;
/*
* Do some preliminary argument and permissions checking before we
* actually start allocating stuff
*/
if (clone_flags & CLONE_NEWUSER) { //clone and new user yes
if (clone_flags & CLONE_THREAD)
return -EINVAL;
/* hopefully this check will go away when userns support is
* complete
*/
if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||
!capable(CAP_SETGID))
return -EPERM;
}
/*
* We hope to recycle these flags after 2.6.26
*/
if (unlikely(clone_flags & CLONE_STOPPED)) {
static int __read_mostly count = 100;
if (count > 0 && printk_ratelimit()) {
char comm[TASK_COMM_LEN];
count--;
printk(KERN_INFO "fork(): process `%s' used deprecated "
"clone flags 0x%lx\n",
get_task_comm(comm, current),
clone_flags & CLONE_STOPPED);
}
}
/*
* When called from kernel_thread, don't do user tracing stuff.
*/
if (likely(user_mode(regs)))
trace = tracehook_prepare_clone(clone_flags);
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL, trace);//把父进程 PCB 的内容复制到新进程的 PCB 中。
/*通过copy_process()函数完成具体的进程创建工作,返回值类型为task_t类型
* 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)) {//函数 IS_ERR()分析copy_process()的返回值是否正确。
struct completion vfork;//定义struct completion 类型的变量 vfork;
trace_sched_process_fork(current, p);
nr = task_pid_vnr(p);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {//判断clone_flags中是否有CLONE_VFORK标志
p->vfork_done = &vfork;
init_completion(&vfork);/*这个函数的作用是在进程创建的最后阶段,父进程会将自己设置为不可中断状态,然后睡眠在
等待队列上(init_waitqueue_head()函数 就是将父进程加入到子进程的等待队列),等待子进程的唤醒。*/
}
audit_finish_fork(p);
tracehook_report_clone(regs, clone_flags, nr, p);
/*
* We set PF_STARTING at creation in case tracing wants to
* use this to distinguish a fully live task from one that
* hasn't gotten to tracehook_report_clone() yet. Now we
* clear it and set the child going.
*/
p->flags &= ~PF_STARTING;
if (unlikely(clone_flags & CLONE_STOPPED)) {
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
__set_task_state(p, TASK_STOPPED);
} else {
wake_up_new_task(p, clone_flags);
}
tracehook_report_clone_complete(trace, regs,
clone_flags, nr, p);
if (clone_flags & CLONE_VFORK) {
freezer_do_not_count();
wait_for_completion(&vfork);
freezer_count();
tracehook_report_vfork_done(p, nr);
}
} else {
nr = PTR_ERR(p);
}
return nr;
}
参考 :https://blog.****.net/u012375924/article/details/87903620
参考 :https://blog.****.net/yiqiaoxihui/article/details/80385546