实验:从整理上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换
学号后三位198
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
1.实验目标
1.分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构
2.使用gdb跟踪分析一个fork系统调用内核处理函数do_fork
3.理解编译链接的过程和ELF可执行文件格式
2.实验环境
VM14pro虚拟机
ubuntu系统(ubuntu-18.04.2-desktop-amd64)
3.阅读task_struct源码
源码来源:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235;
进程是程序的一个执行实例,是正在执行的程序,是能分配处理器并由处理器执行的实体。
进程信息及调度信息
/* -1 unrunnable, 0 runnable, >0 stopped: 进程状态 **/
volatile long state;
unsigned int flags; // 进程状态标志
/** 进程退出 */
int exit_state; int exit_code; int exit_signal;
/** 进程标识号 */
pid_t pid; pid_t tgid;
struct pid *thread_pid;
struct hlist_node pid_links[PIDTYPE_MAX];
/** 用于通知LSM是否被do_execve()函数所调用 */
unsigned in_execve:1;
/** 在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特殊地址*/
struct completion *vfork_done;
/* CLONE_CHILD_SETTID: */
int __user *set_child_tid;
/* CLONE_CHILD_CLEARTID: */
int __user *clear_child_tid;
int prio, static_prio, normal_prio;
unsigned int rt_priority; // 实时进程的优先级
const struct sched_class *sched_class; // 进程调度类
struct sched_entity se; // 普通进程调度实体
struct sched_rt_entity rt; // 实时进程调度实体
unsigned int policy; // 调度策略
分析fork函数
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
具体过程如下:fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_fork()。
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;// ...
// 复制进程描述符,返回创建的task_struct的指针
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;trace_sched_process_fork(current, p);
// 取出task结构体内的pid
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);// 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}// 将子进程添加到调度器的队列,使得子进程有机会获得CPU
wake_up_new_task(p);// ...
// 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间
// 保证子进程优先于父进程运行
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;
}
使用gdb分析
int testFork(int argc, char *argv[]){
pid_t fpid;
int count=0;
fpid=fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("i am the child process, my process id is %d\n",getpid());
count++;
}
else {
printf("i am the parent process, my process id is %d\n",getpid());
count++;
}
printf("result: %d\n",count);
return 0;
}
在 menu 目录下使用 make rootfs 生成文件系统, 然后使用qemu、重新挂载内核
新建一个 shell 窗口,用 gdb 调试该 fork 调用;用以下命令在可能运行的函数处添加断点,跟踪fork执行过程;
编译链接的过程和ELF可执行文件格式
ELF文件在计算机科学中,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件;
ELF有四种不同的类型: 可重定位文件、可执行文件、共享对象文件、核心转储文件。
通过 man elf 命令可查看 elf 文件详细内容。
do_execve函数源代码如下所示:
int do_execve(struct filename *filename,const char __user *const __user *__argv,const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execve_common(filename, argv, envp); // 此处调用do_execve_common
}
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule(),内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
linux-5.0.1/fs/exec.c 文件中的 __do_execve_file 函数功能及部分代码如下:
// 判断文件存在性
if (IS_ERR(filename))
return PTR_ERR(filename);
// 复制一份文件表;
retval = prepare_bprm_creds(bprm);
// 在堆上为文件分配相应空间;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
// 查找并打开二进制文件
if (!file)
file = do_open_execat(fd, filename, flags);
// 等待 CPU 调度来执行该二进制文件
sched_exec();
// 当CPU准备好之后,为该文件执行过程
// 初始化二进制文件描述结构体 linux_binprm
bprm->file = file;
if (!filename) {
bprm->filename = "none";
} else if (fd == AT_FDCWD || filename->name[0] == '/0'){ bprm->filename = filename->name;
} else {
if (filename->name[0] == '\0')
pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
else
pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s", fd, filename->name);
if (!pathbuf) {
retval = -ENOMEM;
goto out_unmark;
}
if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
bprm->filename = pathbuf;
}
bprm->interp = bprm->filename;
// 创建进程的内存地址空间
retval = bprm_mm_init(bprm);
// 填充 linux_binrpm 中的参数
retval = prepare_arg_pages(bprm, argv, envp);
// 检查该二进制文件的可执行权限
retval = prepare_binprm(bprm);
// 从内核空间获取二进制文件的路径名称
retval = copy_strings_kernel(1, &bprm->filename, bprm);
// 调用copy_string()从用户空间拷贝环境变量及命令
retval = copy_strings(bprm->envc, envp, bprm);
// 调用copy_string()从用户空间拷贝命令行参数
retval = copy_strings(bprm->argc, argv, bprm);
// 以上已经打开了二进制可执行文件
// 最终执行该二进制文件
retval = exec_binprm(bprm);
// 后半部分为执行成功的收尾工作
4.总结
通过这次实验,让我更加深入的了解了linux的进程,也了解了代码是怎么变成可执行文件并装入内存的,通过系统调用,用户空间的应用程序就会进入内核空间,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,进程上下文就是一个进程在执行的过程中,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态以便再次执行该进程时,能够恢复切换时的状态,继续执行。