您的位置:首页 > 运维架构 > Linux

linux操作系统分析实验三之从整理上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换

2019-03-25 20:51 585 查看

陈三县+SA18225041+原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ 

一、阅读理解task_struct数据结构

进程描述符都是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。具体代码分析可参考Linux进程管理之task_struct结构体。下图来自深入理解LINUX内核。

二、分析fork函数

2.1 分析do_fork

fork函数是调用了clone函数来实现的,而clone函数中最关键的函数就是do_fork函数。do_fork开始执行后首先做的就是为子进程定义一个新的task_struct指针。在下来会检查一些clone_flags所不允许的位组合 。当一系列的安全检查完毕之后,将会调用copy_process函数。

1)调用dup_task_struct函数为新的进程创建一个内核栈,thread_info结构和task_struct等,当然此时的值都是和父进程完全一样的。dup_task_struct函数如下:

[code]static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
unsigned long *stackend;

int err;

prepare_to_copy(orig);
//为tsk分配内存空间
tsk = alloc_task_struct();
if (!tsk)
return NULL;

//为ti分配内存空间
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}

赋值orig属性给新的tsk
err = arch_dup_task_struct(tsk, orig);
if (err)
goto out;

tsk->stack = ti;

//初始化进程缓存脏数据
err = prop_local_init_single(&tsk->dirties);
if (err)
goto out;

//设置线程栈空间
setup_thread_stack(tsk, orig);
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC;    /* for overflow detection */

#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);
atomic_set(&tsk->fs_excl, 0);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;

account_kernel_stack(ti, 1);

return tsk;

out:
free_thread_info(ti);
free_task_struct(tsk);
return NULL;
}

2)检查并确保新创建该子进程后,当前用户所拥有的进程数没有超出给它分配的资源限制 
代码如下

[code]if (atomic_read(&p->real_cred->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->real_cred->user != INIT_USER)
goto bad_fork_free;
}

3)子进程着手使自己与父进程区别开来,从父进程那继承过来的许多属性都要被清0或设置一个初始值,但task_struct中的大多数数据还是未被修改

[code]     spin_lock_init(&p->alloc_lock);

init_sigpending(&p->pending);

p->utime = cputime_zero;
p->stime = cputime_zero;
p->gtime = cputime_zero;
p->utimescaled = cputime_zero;
p->stimescaled = cputime_zero;
p->prev_utime = cputime_zero;
p->prev_stime = cputime_zero;

p->default_timer_slack_ns = current->timer_slack_ns;

task_io_accounting_init(&p->ioac);
acct_clear_integrals(p);

posix_cpu_timers_init(p);

p->lock_depth = -1;     /* -1 = no lock */
do_posix_clock_monotonic_gettime(&p->start_time);
p->real_start_time = p->start_time;
monotonic_to_bootbased(&p->real_start_time);
p->io_context = NULL;
p->audit_context = NULL;
#ifdef CONFIG_TRACE_IRQFLAGS
p->irq_events = 0;
#ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
p->hardirqs_enabled = 1;
#else
p->hardirqs_enabled = 0;
#endif
p->hardirq_enable_ip = 0;
p->hardirq_enable_event = 0;
p->hardirq_disable_ip = _THIS_IP_;
p->hardirq_disable_event = 0;
p->softirqs_enabled = 1;
p->softirq_enable_ip = _THIS_IP_;
p->softirq_enable_event = 0;
p->softirq_disable_ip = 0;
p->softirq_disable_event = 0;
p->hardirq_context = 0;
p->softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEP
p->lockdep_depth = 0; /* no locks held yet */
p->curr_chain_key = 0;
p->lockdep_recursion = 0;
#endif

#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif

p->bts = NULL;

4)给子进程分配一个CPU

5) 接着就是子进程拷贝父进程的一些资源,调用copy_files函数拷贝父进程打开的文件描述符。

[code]748 static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
{
struct files_struct *oldf, *newf;
int error = 0;

oldf = current->files;
if (!oldf)
goto out;

if (clone_flags & CLONE_FILES) {
//增加文件描述符的引用计数
atomic_inc(&oldf->count);
goto out;
}

//复制文件描述符
newf = dup_fd(oldf, &error);
if (!newf)
goto out;

tsk->files = newf;
error = 0;
out:
return error;
}

6)调用alloc_pid为新进程分配一个pid

[code]pid = alloc_pid(p->nsproxy->pid_ns);
  • 1

7)copy_process做一些收尾工作,并返回新进程的task_struct指针

此时再次回到了do_fork,新创建的子进程被唤醒,并让其先投入运行

[code] 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);
}

 三、使用GDB跟踪分析一个fork系统调用内核处理函数do_fork

[code]cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
make rootfs //修改makefile改成5.0.1,增加-append nokaslr

 

 

[code] gdb
file linux-3.18.6/vmlinux
target remote:1234
//设置断点
b sys_clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_for

在do_fork函数中,以ret_from_fork函数为执行起点,复制父进程的内存堆栈和数据,并修改某些参数实现子进程的定义和初始化,创建子进程的工作完成后,通过sys_call exit函数退出并pop父进程的内存堆栈,实现新进程的创建工作

理解编译链接的过程和ELF可执行文件格式从源文件Hello.c编译链接成Hello.out,需要经历如下步骤:

编译

编译是指编译器读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码。
源文件的编译过程包含两个主要阶段:
第一个阶段是预处理阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。
第二个阶段编译、优化阶段,编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

汇编

汇编实际上指汇编器(as)把汇编语言代码翻译成目标机器指令的过程。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

目标文件(Executable and Linkable Format)

可重定位(Relocatable)文件:由编译器和汇编器生成,可以与其他可重定位目标文件合并创建一个可执行或共享的目标文件;
共享(Shared)目标文件:一类特殊的可重定位目标文件,可以在链接(静态共享库)时加入目标文件或加载时或运行时(动态共享库)被动态的加载到内存并执行;
可执行(Executable)文件:由链接器生成,可以直接通过加载器加载到内存中充当进程执行的文件。

静态链接(编译时)

链接器将函数的代码从其所在地(目标文件或静态链接库中)拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
为创建可执行文件,链接器必须要完成的主要任务:
符号解析:把目标文件中符号的定义和引用联系起来;
重定位:把符号定义和内存地址对应起来,然后修改所有对符号的引用。

动态链接(加载、运行时)

在此种方式下,函数的定义在动态链接库或共享对象的目标文件中。在编译的链接阶段,动态链接库只提供符号表和其他少量信息用于保证所有符号引用都有定义,保证编译顺利通过。动态链接器(ld-linux.so)链接程序在运行过程中根据记录的共享对象的符号定义来动态加载共享库,然后完成重定位。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

加载

加载器把可执行文件从外存加载到内存并进行执行。

进程执行

启动(main函数举例)

内核启动C程序时,会有特殊的启动函数(exec)获取从shell或者父进程来的参数,获取程序入口地址(main函数地址),将这些信息填写到PCB中

使用execve库函数加载一个可执行文件

exec系列函数作用是在进程中加载执行另一个可执行文件,exec 系列函数替换了当前进程(执行该函数的进程)的正文段、数据段、堆和栈(来源于加载的可执行文件)。执行exec 系列函数后从加载可执行文件的main 函数开始重新执行。
execl execle execlp execv execve execvp 六个函数开头均为exec ,所以称为exec 系列函数
PS:exec 系列函数并不创建新进程,所以在调用exec 系列函数后其进程ID 并未改变,已经打开的文件描述符不变。
exec参数如下:

l:表示list,每个命令行参数都说明为一个单独的参数
v:表示vector,命令行参数放在数组中
e:表示由函数调用者提供环境变量表
p:表示通过环境变量PATH来指定路径,查找可执行文件
本次使用execl执行ls命令做跟踪,
函数原型
头文件:unistd.h
int execl(const char pathname,const char arg0, …,NULL);
参数:
pathname:要执行程序的绝对路径名
可变参数:要执行程序的命令行参数,以空指针结束
返回值
出错返回-1
成功该函数不返回!

[code]#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
printf("entering main process---\n");
if(fork()==0){
execl("/bin/ls","ls","-l",NULL);
printf("exiting main process ----\n");}
return 0;
}

总结

子进程是父进程的副本,子进程复制/拷贝父进程的PCB、数据空间(数据段、堆和栈),父子进程共享正文段(只读)
子进程和父进程继续执行fork 函数调用之后的代码,为了提高效率,fork 后不并立即复制父进程数据段、( 堆和栈,采用了写时复制机制(Copy-On-Write) )
当父子进程任意之一要修改数据段、堆、栈时,进行复制操作,并且仅复制修改区域。

五、参考链接

https://www.geek-share.com/detail/2687941622.html

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐