关于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()函数通过如下几个步骤来创建一个新的进程:

  1. 建立新进程的task_struct
  2. 为新进程设置相关的数据结构,如任务数组、自由时间列表等
  3. 启动进程调度

使用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继续执行

3.理解编译链接的过程和ELF可执行文件格式

编译链接的过程如下图所示:

即源文件经过预处理、编译、汇编、链接后生成可执行文件。

而链接又分为静态链接和动态链接:
静态链接:由链接器在链接时将库的内容加入到可执行程序中
动态链接:在可执行文件装载时或运行时,由操作系统的装载程序加载库

ELF:Executable and Linkable Format,可执行链接格式,用于存储Linux程序。包含如下三种:

  1. 可重定向文件:保留代码和适当的数据,与其他的目标文件一起来创建一个可执行文件或共享目标文件。
  2. 可执行文件:保存着可执行的程序
  3. 共享目标文件

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.总结

  1. 为了实现进程的切换,内核需要拥有挂起正在CPU上执行的进程的能力,并具备恢复以前挂起的某个进程的能力
  2. 进程上下文包含了进程执行需要的所有信息如用户地址空间、进程的控制信息、硬件上下文等
  3. schedule()函数会调用switch_to来进行关键上下文的切换

posted on 2019-03-26 21:27  jlc0118  阅读(172)  评论(0编辑  收藏  举报