关于Linux进程创建、可执行文件的加载和进程执行进程切换的分析
学号尾号:155
基于ubuntu kylin 18.10虚拟机
原创作品转载请注明出处https://github.com/mengning/linuxkernel/
1.阅读理解task_struct数据结构
task_struct是Linux内核中的一种数据结构,也即进程的PCB,它会被装载到内存中,并包含了进程的所有信息。如进程的标识、进程的状态、进程的优先级、I/O状态信息、进程间的亲属关系、调度信息等。每个进程都会拥有这样一个task_struct结构。该结构体的部分代码如下:
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;
#ifdef CONFIG_SMP
struct llist_node wake_entry;
int on_cpu;
struct task_struct *last_wakee;
unsigned long wakee_flips;
unsigned long wakee_flip_decay_ts;
...
2.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,//父进程tid地址
int __user *child_tidptr)//子进程tid地址
该函数的源代码如下
long do_fork(unsigned long clone_flags,![image](1F364038251946D38BD074C1A71F671B)
int __user *parent_tidptr,
int __user *child_tidptr)
{
//申请一个task_struct,代表新进程
struct task_struct *p;
int trace = 0;
long nr;
//检查clone_flags的值来确定要进行的操作
if (clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) {
if (clone_flags & (CLONE_THREAD|CLONE_PARENT))
return -EINVAL;
}
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;
}
//将父进程的PCB复制给子进程的PCB,并为其分配pid和user_struct
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
//将新进程加入进程链表和pidhash,增加任务计数值,并使用父进程的上下文来初始化硬件的上下文
if (!IS_ERR(p)) {
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) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
//将新进程挂到就绪队列中,启动进程调度程序,并给父进程返回子进程的pid
if (unlikely(trace))
ptrace_event(trace, nr);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event(PTRACE_EVENT_VFORK_DONE, nr);
}
} else {
nr = PTR_ERR(p);
}
return nr;
}
从以上源代码我们可以看出,do_fork()函数通过如下几个步骤来创建一个新的进程:
- 建立新进程的task_struct
- 为新进程设置相关的数据结构,如任务数组、自由时间列表等
- 启动进程调度
使用gdb跟踪do_fork
首先在menu/test.c文件中添加如下代码
int Fork(int argc, char *argv[])
{
int pid;
pid = fork();
if (pid<0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid==0)
{
/* child process */
printf("This is the child Process!");
}
else
{
/* parent process */
printf("THis is the parent process!");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
MenuConfig("get_param","get the original priority",test_get_param);
MenuConfig("Fork","fork",Fork);
ExecuteMenu();
}
重新制作根文件系统
gcc -pthread -o init linktable.c menu.c test.c -m32 -static
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
使用gdb跟踪调试内核
cd ..
qemu -kernel linux-5.0/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr
此时QEMU窗口处于Stopped的状态,接着另开一个shell
gdb
file vmlinux # target remote之前加载符号表
target remote:1234 # 建立gdb和gdbserver之间的连接,按c让qemu上的Linux系统继续运行
接着在如下地方打下断点
b sys_clone
b _do_fork
b dup_task_struct
b copy_process
接着依次按c继续执行
![](https://img2018.cnblogs.com/blog/1492512/201903/1492512-20190326212826943-168838690.png)
3.理解编译链接的过程和ELF可执行文件格式
编译链接的过程如下图所示:
即源文件经过预处理、编译、汇编、链接后生成可执行文件。
而链接又分为静态链接和动态链接:
静态链接:由链接器在链接时将库的内容加入到可执行程序中
动态链接:在可执行文件装载时或运行时,由操作系统的装载程序加载库
ELF:Executable and Linkable Format,可执行链接格式,用于存储Linux程序。包含如下三种:
- 可重定向文件:保留代码和适当的数据,与其他的目标文件一起来创建一个可执行文件或共享目标文件。
- 可执行文件:保存着可执行的程序
- 共享目标文件
4.编程使用exec*库函数加载一个可执行文件
创建一个exec.c文件,并在其中输入如下代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
if(fork()==0)
{
execl("/bin/ls","ls","-l",NULL);
}
return 0;
}
使用gcc编译并执行后,输出的结果与在shell中执行ls -l命令的结果一致
5.总结
- 为了实现进程的切换,内核需要拥有挂起正在CPU上执行的进程的能力,并具备恢复以前挂起的某个进程的能力
- 进程上下文包含了进程执行需要的所有信息如用户地址空间、进程的控制信息、硬件上下文等
- schedule()函数会调用switch_to来进行关键上下文的切换