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

《Linux内核分析》第六周学习笔记

2016-03-29 22:18 761 查看

《Linux内核分析》第六周学习笔记 进程的描述和创建

郭垚 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

【学习视频时间:1小时 撰写博客时间:2小时】

【学习内容:进程创建的过程、使用gdb跟踪分析内核处理函数sys_clone】

一、进程的描述

1.1 进程描述符task_struct数据结构(一)

1. 进程控制块PCB——task_struct

为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。

struct task_struct{
volatile long state; //进程状态,-1表示不可执行,0表示可执行,大于1表示停止
void *stack; //内核堆栈
atomic_t usage;
unsigned int flags; //进程标识符
unsigned int ptrace;
……
}


struct task_ struct数据结构很庞大

Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_ RUNNING,为什么呢?

进程的标示pid

所有进程链表struct list_ head tasks;

内核的双向循环链表的实现方法

一个更简略的双向循环链表

程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系

Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_ info和进程的内核堆栈

进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?

内核控制路径所用的堆栈很少,因此对栈和Thread_ info来说,8KB足够了

struct thread_ struct thread; //CPU-specific state of this task

文件系统和文件描述符

内存管理——进程的地址空间


Linux进程状态转换图

1.2 进程描述符task_struct数据结构(二)

1242#ifdef CONFIG_SMP //条件编译,多处理器会用到
1243    struct llist_node wake_entry;
1244    int on_cpu;
1245    struct task_struct *last_wakee;
1246    unsigned long wakee_flips;
1247    unsigned long wakee_flip_decay_ts;
1248
1249    int wake_cpu;
1250#endif
1251    int on_rq;
1252
1253    int prio, static_prio, normal_prio;
1254    unsigned int rt_priority; //与优先级相关
1255    const struct sched_class *sched_class;
1256    struct sched_entity se;
1257    struct sched_rt_entity rt;

……

1295    struct list_head tasks; //进程链表
1296#ifdef CONFIG_SMP
1297    struct plist_node pushable_tasks;
1298    struct rb_node pushable_dl_tasks;
1299#endif




Linux的状态图

1301    struct mm_struct *mm, *active_mm; //内存管理进程的地址空间
1302#ifdef CONFIG_COMPAT_BRK
1303    unsigned brk_randomized:1;
1304#endif
1305    /* per-thread vma caching */
1306    u32 vmacache_seqnum;
1307    struct vm_area_struct *vmacache[VMACACHE_SIZE];
1308#if defined(SPLIT_RSS_COUNTING)
1309    struct task_rss_stat    rss_stat;
1310#endif
1311/* task state */
1312    int exit_state;  //任务的状态
1313    int exit_code, exit_signal;
1314    int pdeath_signal;  /*  The signal sent when the parent dies  */
1315    unsigned int jobctl;    /* JOBCTL_*, siglock protected */
1316
1317    /* Used for emulating ABI behavior of previous Linux versions */
1318    unsigned int personality;
1319
1320    unsigned in_execve:1;   /* Tell the LSMs that the process is doing an
1321                 * execve */
1322    unsigned in_iowait:1;

……

1337    //进程的父子关系
1338     * pointers to (original) parent process, youngest child, younger sibling,
1339     * older sibling, respectively.  (p->father can be replaced with
1340     * p->real_parent->pid)
1341     */
1342    struct task_struct __rcu *real_parent; /* real parent process */
1343    struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
1344    /*
1345     * children/sibling forms the list of my natural children
1346     */
1347    struct list_head children;  /* list of my children */
1348    struct list_head sibling;   /* linkage in my parent's children list */
1349    struct task_struct *group_leader;   /* threadgroup leader */






1356    struct list_head ptraced; //调试
1357    struct list_head ptrace_entry;
1358
1359    /* PID/PID hash table linkage. */
1360    struct pid_link pids[PIDTYPE_MAX];
1361    struct list_head thread_group;
1362    struct list_head thread_node; //将进程链表连接起来

……

1411/* 与当前任务CPU状态相关,对进程上下文切换有关键性作用 */
1412    struct thread_struct thread;
1413/* filesystem information */
1414    struct fs_struct *fs;
1415/* 打开文件描述符列表 */
1416    struct files_struct *files;
1417/* namespaces */
1418    struct nsproxy *nsproxy;
1419/* 信号处理 */
1420    struct signal_struct *signal;
1421    struct sighand_struct *sighand;
1422
1423    sigset_t blocked, real_blocked;
1424    sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
1425    struct sigpending pending;
……
1598    struct pipe_inode_info *splice_pipe;
1599    //与管道相关
1600    struct page_frag task_frag;
1601
1602#ifdef  CONFIG_TASK_DELAY_ACCT
1603    struct task_delay_info *delays;
1604#endif
1605#ifdef CONFIG_FAULT_INJECTION
1606    int make_it_fail;
1607#endif


二、进程的创建

2.1 进程的创建概览及fork一个进程的用户态代码

回顾:

道生一(start_ kernel...cpu_ idle)

一生二(kernel_ init和kthreadd)

二生三(即前面的0、1、2三个进程)

三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)

start_ kernel创建了cpu_ idle,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;另一个是kthreadd。0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的

iret与int 0x80指令对应,一个是弹出寄存器值,一个是压入寄存器的值

如果将系统调用类比于fork();那么就相当于系统调用创建了一个子进程,然后子进程返回之后将在内核态运行,而返回到父进程后仍然在用户态运行

fork一个子进程的代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0) //pid == 0和下面的else都会被执行到(一个是在父进程中即pid ==0的情况,一个是在子进程中,即pid不等于0)

{
/* child process */
printf("This is Child Process!\n");
}
else
{
/* parent process  */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}


2.2 理解进程创建过程复杂代码的方法





Linux中创建进程一共有三个函数:

fork,创建子进程

vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。

clone,主要用于创建线程

  这里值得注意的是,Linux中得线程是通过模拟进程实现的,较新的内核使用的线程库一般都是NPTL。

复制一个PCB——task_struct

err = arch_dup_task_struct(tsk, orig);


给新进程分配一个新的内核堆栈

ti = alloc_ thread_ info_ node(tsk, node);
tsk->stack = ti;
setup_ thread_ stack(tsk, orig); //这里只是复制thread_ info,而非复制内核堆栈


要修改复制过来的进程数据,比如pid、进程链表等。具体见copy _process内部

从用户态的代码看fork(),函数返回了两次,即在父子进程中各返回一次。

2.3 浏览进程创建过程相关的关键代码

通过之前的学习,我们知道fork是通过触发0x80中断,陷入内核,来使用内核提供的提供调用:

SYSCALL_DEFINE0(fork)
{
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
#endif

SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}


通过以上精简后的代码,我们可以看出,fork、vfork和clone这三个函数最终都是通过do_ fork函数实现的。

追踪do_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;
}


由以上代码可以看出,do_ fork大概做了如下几件事:

调用copy_ process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。

初始化vfork的完成处理信息(如果是vfork调用)

调用wake_ up_ new_ task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。

如果是vfork调用,需要阻塞父进程,知道子进程执行exec。上面的过程对vfork稍微做了处理,因为vfork必须保证子进程优先运行,执行exec,替换自己的地址空间。

抛开vfork,进程创建的大部分过程都在copy_ process函数中copy_process的代码非常复杂,这里我精简了大部分,只留下最重要的一些:

/*
创建进程描述符以及子进程所需要的其他所有数据结构
为子进程准备运行环境
*/
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;

// 分配一个新的task_struct,此时的p与当前进程的task,仅仅是stack地址不同
p = dup_task_struct(current);

// 检查该用户的进程数是否超过限制
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
// 检查该用户是否具有相关权限,不一定是root
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}

retval = -EAGAIN;
// 检查进程数量是否超过 max_threads,后者取决于内存的大小
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;

// 初始化自旋锁

// 初始化挂起信号

// 初始化定时器

// 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING
retval = sched_fork(clone_flags, p);
// .....

// 复制所有的进程信息
// copy_xyz

// 初始化子进程的内核栈
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
goto bad_fork_cleanup_io;

if (pid != &init_struct_pid) {
retval = -ENOMEM;
// 这里为子进程分配了新的pid号
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (!pid)
goto bad_fork_cleanup_io;
}

/* ok, now we should be set up.. */
// 设置子进程的pid
p->pid = pid_nr(pid);
// 如果是创建线程
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
// 线程组的leader设置为当前线程的leader
p->group_leader = current->group_leader;
// tgid是当前线程组的id,也就是main进程的pid
p->tgid = current->tgid;
} else {
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
// 创建的是进程,自己是一个单独的线程组
p->group_leader = p;
// tgid和pid相同
p->tgid = p->pid;
}

if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
// 如果是创建线程,那么同一线程组内的所有线程、进程共享parent
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
} else {
// 如果是创建进程,当前进程就是子进程的parent
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
}

// 将pid加入PIDTYPE_PID这个散列表
attach_pid(p, PIDTYPE_PID);
// 递增 nr_threads的值
nr_threads++;

// 返回被创建的task结构体指针
return p;
}


根据这份精简代码,我总结出copy_process的大体流程

检查各种标志位(已经省略)

调用dup_ task_ struct复制一份task_ struct结构体,作为子进程的进程描述符。

检查进程的数量限制。

初始化定时器、信号和自旋锁。

初始化与调度有关的数据结构,调用了sched_ fork,这里将子进程的state设置为TASK_ RUNNING。

复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等。

调用copy_ thread,这又是关键的一步,这里设置了子进程的堆栈信息。

为子进程分配一个pid

设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。

2.4 创建的新进程从哪里开始执行?

一个新创建的子进程,(当它获得CPU之后)是从哪一行代码进程执行的?

与之前写过的my_ kernel相比较,kernel中是可以指定新进程开始的位置(也就是通过eip寄存器指定代码行)。fork中也有相似的机制

这涉及子进程的内核堆栈数据状态和task_ struct中thread记录的sp和ip的一致性问题,这是在copy_ thread in copy_ process设定的

*childregs = *current_pt_regs(); //复制内核堆栈
childregs->ax = 0; //子进程的fork返回0
p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址


2.5 使用gdb跟踪创建新进程的过程(实验)

1. 更新menu,删除test_fork.c和test.c文件,重新执行make rootfs



2. 编译内核查看fork命令



3. 启动gdb调试,并对主要的函数设置断点







执行一个fork,会发现只输出一个fork的命令描述,后面并没有执行,因为它停在了sys_ clone这个位置。

4. 特别关注新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?即执行起点与内核堆栈如何保证一致。

  答:ret_ from_ fork决定了新进程的第一条指令地址。子进程从ret_ from_ fork处开始执行。因为在ret_ from_ fork之前,也就是在copy_ thread()函数中* childregs = * current_ pt_ regs();该句将父进程的regs参数赋值到子进程的内核堆栈。* childregs的类型为pt_ regs,里面存放了SAVE_ ALL中压入栈的参数,因此在之后的RESTORE ALL中能顺利执行下去。

总结

  Linux中所有的进程创建都是基于复制的方式,Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现。然后对子进程做一些特殊的处理。而Linux中的线程,又是一种特殊的进程。根据代码的分析,do_ fork中,copy_ process管子进程运行的准备,wake_ up_ new_ task作为子进程forking的完成。fork()函数最大的特点就是被调用一次,返回两次。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: