来自 服务器&运维 2020-05-06 07:13 的文章
当前位置: 澳门威尼斯人平台 > 服务器&运维 > 正文

Linux内核创建一个进程的过程分析

不管在什么系统中,所有的任务都是以进程为载体的,所以理解进程的创建对于理解操作系统的原理是非常重要的,本文是我在学习linux内核中所做的笔记,如有错误还请大家批评指正。注:我所阅读的内核版本是0.11。

使用版本: linux-2.6.22.9

.
ret_from_fork

曹朋辉
原创作品转载请注明出处
《Linux内核分析》MOOC课程

一、关于PCB

对于一个进程来说,PCB就好像是他的记账先生,当一个进程被创建时PCB就被分配,然后有关进程的所有信息就全都存储在PCB中,例如,打开的文件,页表基址寄存器,进程号等等。在linux中PCB是用结构task_struct来表示的,我们首先来看一下task_struct的组成。

代码位于linux/include/linux/Sched.h

struct task_struct {

    long state; //表示进程的状态,-1表示不可执行,0表示可执行,>0表示停止
    long counter;/* 运行时间片,以jiffs递减计数 */
    long priority; /* 运行优先数,开始时,counter = priority,值越大,表示优先数越高,等待时间越长. */
    long signal;/* 信号.是一组位图,每一个bit代表一种信号. */
    struct sigaction sigaction[32]; /* 信号响应的数据结构, 对应信号要执行的操作和标志信息 */
    long blocked;   /* 进程信号屏蔽码(对应信号位图) */
/* various fields */
    int exit_code; /* 任务执行停止的退出码,其父进程会取 */
    unsigned long start_code,end_code,end_data,brk,start_stack;/* start_code代码段地址,end_code代码长度(byte),
end_data代码长度+数据长度(byte),brk总长度(byte),start_stack堆栈段地址 */
    long pid,father,pgrp,session,leader;/* 进程号,父进程号 ,父进程组号,会话号,会话头(发起者)*/
    unsigned short uid,euid,suid;/* 用户id 号,有效用户 id 号,保存用户 id 号*/
    unsigned short gid,egid,sgid;/* 组标记号 (组id),有效组 id,保存的组id */
    long alarm;/* 报警定时值 (jiffs数) */
    long utime,stime,cutime,cstime,start_time;/* 用户态运行时间 (jiffs数),
系统态运行时间 (jiffs数),子进程用户态运行时间,子进程系统态运行时间,进程开始运行时刻 */
    unsigned short used_math;/* 是否使用了协处理器 */
/* file system info */
    int tty;        /* 进程使用tty的子设备号. -1表示设有使用 */
    unsigned short umask; /* 文件创建属性屏蔽位 */
    struct m_inode * pwd; /* 当前工作目录 i节点结构 */
    struct m_inode * root; /* 根目录i节点结构 */
    struct m_inode * executable;/* 执行文件i节点结构 */
    unsigned long close_on_exec; /* 执行时关闭文件句柄位图标志. */
    struct file * filp[NR_OPEN];
/* 文件结构指针表,最多32项. 表项号即是文件描述符的值 */
    struct desc_struct ldt[3];
/* 任务局部描述符表.0-空,1-cs段,2-Ds和Ss段 */
    struct tss_struct tss; /* 进程的任务状态段信息结构 */
};

Fork的系统调用代码在linux/arch/i386/kernel/process.c中:

分析fork函数对应的系统调用处理过程

启动保护fork命令的menuOS

图片 1

设置断点进行调试

图片 2

图片 3

内核里操作系统的三大功能:
内存管理
进程管理
文件系统
其中最核心的是进程管理

二、进程的创建

系统中的进程是由父进程调用fork()函数来创建的,那么调用fork()函数的时候究竟会发生什么呢?

      asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}

进程的创建

从了解进程的创建,进程间调度切换,来从总体把握进程工作

当前进程复制一个子进程,然后进行修改

fork一个子进程的代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc , char * argv[])
{
    int pid;
    pid = fork();
    if(pid<0)
    {
        fprintf(stderr,"Fork Failed !");
        exit(-1);
    }
    else if (pid==0)
    {
        printf("This is Child Process!n");
     }
    else
    {
        printf("This is Parent Process !n");
        wait(NULL);
        printf("Child Complete!n:);
    }

}

-fork()是用户态用于创建子进程的系统调用
-pid<0时打印出错信息
-子进程中fork()返回值为0,父进程中fork()返回值为进程pid值
-所以else if和else都将被执行
理解进程创建过程复杂代码的方法

图片 4

创建一个新进程在内核中的执行过程
-fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建
-创建新进程是通过复制当前进程来实现
-复制一个PCB task_struct
-给新进程分配一个新内核堆栈
-修改进程数据如pid、进程链表等
-从用户态代码中可以看到fork()函数返回两次,在父进程和子进程中各返回一次,父进程从系统调用中返回,子进程从系统调用中返回涉及了子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,在copy_thread in copy_process里设定

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;

    /*
     * Determine whether and which event to report to ptracer.  When
     * called from kernel_thread or CLONE_UNTRACED is explicitly
     * requested, no event is reported; otherwise, report if the event
     * for the type of forking is enabled.
     */
    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;
    }

    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
    /*
     * Do this prior waking up the new thread - the thread pointer
     * might get invalid after that point, if the thread exits quickly.
     */
    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

        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);

        /* forking complete and child started to run, tell ptracer */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);

        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;
}

copy process创建进程内容
-dup_task_struct复制pcb,分配新空间
-然后初始化赋值子进程信息
-copy thread从子进程pid内核堆栈位置栈底,拷贝内核堆栈数据和指定新进程的第一条指令地址

进程描述符task_struct数据结构

1、引发0×80中断

进程1是由进程0通过fork()创建的,其中的fork代码如下:

init/main.c

#define _syscall0(type,name) /   
type name(void) /  
{ /  
long __res; /  
__asm__ volatile ( "int $0x80" /    // 调用系统中断0x80。   
:"=a" (__res) /     // 返回值??eax(__res)。   
:"0" (__NR_##name)); /           // 输入为系统中断调用号__NR_name。   
      if (__res >= 0) /      // 如果返回值>=0,则直接返回该值。   
      return (type) __res; errno = -__res; /    // 否则置出错号,并返回-1。   
      return -1;}

这样使用int 0×80中断,调用sys_fork系统调用来创建进程。

Sys_fork系统调用通过 do_fork()函数实现,通过对do_fork()函数传递不同的clone_flags来实现fork,clone,vfork。

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

·p->thread.ip = (unsigned long) ret_from_fork;·
ret_from_fork返回了子进程调度的第一条指令
在复制内核堆栈时只复制了其中一部分SAVE_ALLL相关的部分,int指令和cpu压栈内容,即最栈底的部分
ret_from_fork会跳转syscall exit,最终返回用户态,此时返回到子进程用户空间
所以创建的新进程从ret_from_fork开始执行

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

进程控制块PCB——task_struct
为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。
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
文件系统和文件描述符
内存管理——进程的地址空间

2、sys_fork()

_sys_fork:  
call _find_empty_process # 调用find_empty_process()(kernel/fork.c,135)。  
testl %eax,%eax  
js 1f  
push %gs  
pushl %esi  
pushl %edi  
pushl %ebp  
pushl %eax  
call _copy_process # 调用C 函数copy_process()(kernel/fork.c,68)。  
addl $20,%esp # 丢弃这里所有压栈内容。  
1: ret

虽然是一段汇编代码,但是我们可以很清楚的看到首先调用的是find_empty_process(),然后又调用了copy_process(),而这两个函数就是fork.c中的函数。下面我们来看一下这两个函数。

Syn_clone和syn_vfork的系统调用代码如下:

图片 5

本文由澳门威尼斯人平台发布于服务器&运维,转载请注明出处:Linux内核创建一个进程的过程分析

关键词: