<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=450&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / linux源码解读（一）：进程的创建、调度和销毁]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=450</link>
		<description><![CDATA[linux源码解读（一）：进程的创建、调度和销毁 最近发表的帖子。]]></description>
		<lastBuildDate>Sun, 09 Oct 2022 02:44:32 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[linux源码解读（一）：进程的创建、调度和销毁]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=457#p457</link>
			<description><![CDATA[<p>不论是做正向开发，还是逆向破解，操作系统、编译原理、数据结构和算法、计算机组成原理、计算机网络、密码学等都是非常核心和关键的课程。为了便于理解操作系统原理，这里从linux 0.11开始解读重要和核心的代码！简单理解：操作系统=计算机组成原理+数据结构和算法！</p><p>&#160; 用户从开机上电开始，cpu会先用bios读取初始化代码，这一系列的初始化流程请详见本人之前撰写的x86系列教程，这里不再赘述；先看一下代码的整体结构，从文件名就能很容易猜出来各个模块分别是干啥的：硬件启动、文件系统、头文件、整个初始化、内核、库函数、内存管理、工具等；</p><p>&#160; &#160; &#160; &#160; &#160;linux代码大部分是C写的，所以入口也不免俗，也是用main开始的，具体就是init/main.c文件的main函数，主要干了这么几件事：设置根文件和驱动、设置内存、初始化idt表、打开中断、切换到用户模式；从这里可以看出：前面这些代码执行的时候是屏蔽了中断的，所以在这个阶段，用户移动鼠标、敲击键盘什么的都是没用的！这些准备工作都做完后，终于生成了“永世不灭”的0号进程：从linus的注释看，只要没有其他任务运行了就运行0号进程；pause函数也只是查看是否有其他可以运行的任务。如果有就跳转过去运行，如果没有继续在这里死循环！</p><p>&#160; 注意：这里面有个buffer_memory_end字段，标识了内核缓冲区；一般情况下：cpu往块设备写数据，会先写入这里的缓存，缓存到一定数量后统一写入设备，能提升一些效率；比如本人之前用ida去trace x音时，log文件并不是实时更新的；要等trace借结束后才会统一写入磁盘的log文件！</p><div class="codebox"><pre class="vscroll"><code>//main函数 linux引导成功后就从这里开始运行
void main(void)        /* This really IS void, no error here. */
{            /* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
//前面这里做的所有事情都是在对内存进行拷贝
     ROOT_DEV = ORIG_ROOT_DEV;//设置操作系统的根文件
     drive_info = DRIVE_INFO;//设置操作系统驱动参数
     //解析setup.s代码后获取系统内存参数
    memory_end = (1&lt;&lt;20) + (EXT_MEM_K&lt;&lt;10);
    //取整4k的内存大小
    memory_end &amp;= 0xfffff000;
    if (memory_end &gt; 16*1024*1024)//控制操作系统的最大内存为16M
        memory_end = 16*1024*1024;
    if (memory_end &gt; 12*1024*1024) 
        buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小，跟块设备有关，跟设备交互的时候，充当缓冲区，写入到块设备中的数据先放在缓冲区里，只有执行sync时才真正写入；这也是为什么要区分块设备驱动和字符设备驱动；块设备写入需要缓冲区，字符设备不需要是直接写入的
    else if (memory_end &gt; 6*1024*1024)
        buffer_memory_end = 2*1024*1024;
    else
        buffer_memory_end = 1*1024*1024;
    main_memory_start = buffer_memory_end;
#ifdef RAMDISK
    main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
//内存控制器初始化
    mem_init(main_memory_start,memory_end);
    //异常函数初始化，主要是初始化idt表
    trap_init();
    //块设备驱动初始化
    blk_dev_init();
    //字符型设备出动初始化
    chr_dev_init();
    //控制台设备初始化
    tty_init();
    //加载定时器驱动
    time_init();
    //进程间调度初始化
    sched_init();
    //缓冲区初始化
    buffer_init(buffer_memory_end);
    //硬盘初始化
    hd_init();
    //软盘初始化
    floppy_init();
    sti();
    //从内核态切换到用户态，上面的初始化都是在内核态运行的
    //内核态无法被抢占，不能在进程间进行切换，运行不会被干扰
    move_to_user_mode();
    if (!fork()) {    //创建0号进程 fork函数就是用来创建进程的函数    /* we count on this going ok */
        //0号进程是所有进程的父进程
        init();
    }
/*
 *   NOTE!!   For any other task &#039;pause()&#039; would mean we have to get a
 * signal to awaken, but task 0 is the sole exception (see &#039;schedule()&#039;)
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 &#039;pause()&#039; just means we go check if some other
 * task can run, and if not we return here.
 */
//0号进程永远不会结束，他会在没有其他进程调用的时候调用，只会执行for(;;) pause();
    for(;;) pause();
}</code></pre></div><p>&#160; &#160;1、main中有个非常核心的函数：fork；linux中所有的进程创建都通过fork函数；这个函数本质上是个系统调用，实现代码在system_call.s中，如下：</p><div class="codebox"><pre><code>_sys_fork://fork的系统调用
    call _find_empty_process//调用这个函数
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process//
    addl $20,%esp
1:    ret</code></pre></div><p>&#160; 整个fork的执行过程并不复杂：先是找空进程，再复制进程，这两个函数到底是怎么做的了？在kernel/fork.c中有他们的实现过程，先来看看find_empty_process: 这个版本的NR_TASKS=64，也就是说最多支持64个进程“同时”运行（直观感觉这算个漏洞啊，如果别有用心的人想办法短时间内恶意把进程数提升到64个，是不是就没法运行新的程序了？间接达到DOS的效果）！整个代码很简单：直接遍历64个task_struct结构体，看看哪个结构体还是null的，说明这个结构体还未初始化，没被使用，直接返回这个task结构体在数组的index。这个index也被用来作为进程的编号，也就是pid！</p><div class="codebox"><pre><code>int find_empty_process(void)
{
    int i;

    repeat:
        if ((++last_pid)&lt;0) last_pid=1;
        for(i=0 ; i&lt;NR_TASKS ; i++)
            if (task[i] &amp;&amp; task[i]-&gt;pid == last_pid) goto repeat;
    for(i=1 ; i&lt;NR_TASKS ; i++)
        if (!task[i])//直到找到一个空的task结构体
            return i;
    return -EAGAIN;//达到64的最大值后，返回错误码
}</code></pre></div><p>&#160; 一旦找到空的进程（实际上是还没使用的task结构体，就是所谓的进程槽），继续执行copy_process，这里又拷贝了啥了？</p><p>新建一个task结构体，并分配内存<br />根据上一步得到的pid，把task结构体保存在这个index<br />用传入的参数初始化task结构体（主要是context寄存器）<br />设置子进程的ldt，并复制父进程的数据段（注意：不复制代码段，否则没必要生成子进程了）<br />父进程打开过的文件，子进程继承该属性<br />在gdt中设置该子进程的tss和ldt<br />子进程设置成运行态<br />&#160; 总结：函数名称叫copy_process，实际上只是拷贝了父进程的数据段，继承了父进程打开文件的数量，其他都是“个性化”设置的，所以linus当初为啥要取名为copy_process了？这里很不解！</p><div class="codebox"><pre class="vscroll"><code>// 对内存拷贝
// 主要作用就是把代码段数据段等栈上的数据拷贝一份
int copy_mem(int nr,struct task_struct * p)
{
    unsigned long old_data_base,new_data_base,data_limit;
    unsigned long old_code_base,new_code_base,code_limit;

    code_limit=get_limit(0x0f);
    data_limit=get_limit(0x17);
    old_code_base = get_base(current-&gt;ldt[1]);
    old_data_base = get_base(current-&gt;ldt[2]);
    if (old_data_base != old_code_base)
        panic(&quot;We don&#039;t support separate I&amp;D&quot;);
    if (data_limit &lt; code_limit)
        panic(&quot;Bad data_limit&quot;);
    //数据段和代码段的base地址是一样的
    new_data_base = new_code_base = nr * 0x4000000;
    //设置新进程代码入口地址
    p-&gt;start_code = new_code_base;
    //设置新进程的idt
    set_base(p-&gt;ldt[1],new_code_base);
    set_base(p-&gt;ldt[2],new_data_base);
    //新进程直接简单粗暴地复制了父进程的数据，但是代码并未复用
    if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
        free_page_tables(new_data_base,data_limit);
        return -ENOMEM;
    }
    return 0;
}

/*
 *  Ok, this is the main fork-routine. It copies the system process
 * information (task[nr]) and sets up the necessary registers. It
 * also copies the data segment in it&#039;s entirety.
 */
// 所谓进程创建就是对0号进程或者当前进程的复制
// 就是结构体的复制 把task[0]对应的task_struct 复制一份
//除此之外还要对栈堆拷贝 当进程做创建的时候要复制原有的栈堆
// nr就是刚刚找到的空槽的pid
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
        long ebx,long ecx,long edx,
        long fs,long es,long ds,
        long eip,long cs,long eflags,long esp,long ss)
{
    struct task_struct *p;
    int i;
    struct file *f;
    //其实就是malloc分配内存
    p = (struct task_struct *) get_free_page();//在内存分配一个空白页，让指针指向它
    if (!p)
        return -EAGAIN;//如果分配失败就是返回错误
    task[nr] = p;//把这个指针放入进程的链表当中
    *p = *current;//把当前进程赋给p，也就是拷贝一份    /* NOTE! this doesn&#039;t copy the supervisor stack */
    //后面全是对这个结构体进行赋值相当于初始化赋值
    p-&gt;state = TASK_UNINTERRUPTIBLE;
    p-&gt;pid = last_pid;
    p-&gt;father = current-&gt;pid;
    p-&gt;counter = p-&gt;priority;
    p-&gt;signal = 0;
    p-&gt;alarm = 0;
    p-&gt;leader = 0;        /* process leadership doesn&#039;t inherit */
    p-&gt;utime = p-&gt;stime = 0;
    p-&gt;cutime = p-&gt;cstime = 0;
    p-&gt;start_time = jiffies;//当前的时间
    p-&gt;tss.back_link = 0;
    p-&gt;tss.esp0 = PAGE_SIZE + (long) p;
    p-&gt;tss.ss0 = 0x10;
    p-&gt;tss.eip = eip;
    p-&gt;tss.eflags = eflags;
    p-&gt;tss.eax = 0;//把寄存器的参数添加进来
    p-&gt;tss.ecx = ecx;
    p-&gt;tss.edx = edx;
    p-&gt;tss.ebx = ebx;
    p-&gt;tss.esp = esp;
    p-&gt;tss.ebp = ebp;
    p-&gt;tss.esi = esi;
    p-&gt;tss.edi = edi;
    p-&gt;tss.es = es &amp; 0xffff;
    p-&gt;tss.cs = cs &amp; 0xffff;
    p-&gt;tss.ss = ss &amp; 0xffff;
    p-&gt;tss.ds = ds &amp; 0xffff;
    p-&gt;tss.fs = fs &amp; 0xffff;
    p-&gt;tss.gs = gs &amp; 0xffff;
    p-&gt;tss.ldt = _LDT(nr);
    p-&gt;tss.trace_bitmap = 0x80000000;
    if (last_task_used_math == current)//如果使用了就设置协处理器
        __asm__(&quot;clts ; fnsave %0&quot;::&quot;m&quot; (p-&gt;tss.i387));
    if (copy_mem(nr,p)) {//老进程向新进程代码段和数据段进行拷贝
        task[nr] = NULL;//如果失败了
        free_page((long) p);//就释放当前页
        return -EAGAIN;
    }
    for (i=0; i&lt;NR_OPEN;i++)//
        if (f=p-&gt;filp[i])//父进程打开过文件
            f-&gt;f_count++;//就会打开文件的计数+1，说明会继承这个属性
    if (current-&gt;pwd)//跟上面一样
        current-&gt;pwd-&gt;i_count++;
    if (current-&gt;root)
        current-&gt;root-&gt;i_count++;
    if (current-&gt;executable)
        current-&gt;executable-&gt;i_count++;
    //设置GDT表的tss和ldt
    set_tss_desc(gdt+(nr&lt;&lt;1)+FIRST_TSS_ENTRY,&amp;(p-&gt;tss));
    set_ldt_desc(gdt+(nr&lt;&lt;1)+FIRST_LDT_ENTRY,&amp;(p-&gt;ldt));
    p-&gt;state = TASK_RUNNING;//把状态设定为运行状态    /* do this last, just in case */
    return last_pid;//返回新创建进程的id号
}</code></pre></div><p>&#160; &#160;fork执行结束后，如果是0号进程（也就是父进程）成功创建，会继续执行init函数：这个函数内部的代码也很特别，不知道大家有没有注意到：里面有个while(1)循环，而且没有break打断，也就是说这里面的代码会一直执行，直到被中断/系统调用等方式打断；等中断/系统调用执行万后又会遇到这里继续执行！作者本意因该是父进程只负责创建子进程，创建好的子进程来执行/bin/sh，周而复始一直循环执行！所以问题又来了：为什么要不停的生成子进程去执行/bin/sh了？sh是用来接受用户输入并执行的程序，为了让用户随时随地可以输入指令，这里只好不停地生成进程执行/bin/sh了！</p><div class="codebox"><pre class="vscroll"><code>void init(void)
{
    int pid,i;
    //设置了驱动信息
    setup((void *) &amp;drive_info);
    //打开标准输入控制台 句柄为0, 方便进程交互
    (void) open(&quot;/dev/tty0&quot;,O_RDWR,0);
    (void) dup(0);//打开标准输入控制台 这里是复制句柄的意思
    (void) dup(0);//打开标准错误控制台
    printf(&quot;%d buffers = %d bytes buffer space\n\r&quot;,NR_BUFFERS,
        NR_BUFFERS*BLOCK_SIZE);
    printf(&quot;Free mem: %d bytes\n\r&quot;,memory_end-main_memory_start);
    if (!(pid=fork())) {//这里创建1号进程
        close(0);//关闭了0号进程的标准输入输出
        if (open(&quot;/etc/rc&quot;,O_RDONLY,0))//如果1号进程创建成功打开/etc/rc这里面保存的大部分是系统配置文件 开机的时候要什么提示信息全部写在这个里面
            _exit(1);
        execve(&quot;/bin/sh&quot;,argv_rc,envp_rc);//运行shell程序
        _exit(2);
    }
    if (pid&gt;0)//如果这个不是0号进程
        while (pid != wait(&amp;i))//就等待父进程退出，等待期间啥也不干
            /* nothing */;
    while (1) {
        if ((pid=fork())&lt;0) {//再创建新进程：如果创建失败
            printf(&quot;Fork failed in init\r\n&quot;);
            continue;
        }
        //如果创建成功
        if (!pid) {//这段代码是在子进程执行的
            close(0);close(1);close(2);//关闭上面那几个输入输出错误的句柄
            setsid();//重新设置id
            (void) open(&quot;/dev/tty0&quot;,O_RDWR,0);
            (void) dup(0);
            (void) dup(0);//重新打开
            _exit(execve(&quot;/bin/sh&quot;,argv,envp));//这里不是上面的argv_rc和envp_rc了是因为怕上面那种创建失败，换了一种环境变量来创建，过程和上面是一样的其实
        }
        //这段代码是在父进程执行的：如果还在父进程，那么等待子进程结束退出，并重新开始循环
        while (1)
            if (pid == wait(&amp;i))
                break;
        printf(&quot;\n\rchild %d died with code %04x\n\r&quot;,pid,i);
        sync();
    }
    _exit(0);    /* NOTE! _exit, not exit() */
}</code></pre></div><p>&#160; 进程相关重要的结构体： task_struct：描述了task的方方面面，比如时间片、优先级、信号、pid、运行时间、ldt、tss等，每个进程都会生成一个task结构体来存储该进程的所有属性！</p><div class="codebox"><pre class="vscroll"><code>//task即进程的意思，这个结构体把进程能用到的所有信息进行了封装
struct task_struct {
/* these are hardcoded - don&#039;t touch */
    long state;    //程序运行的状态/* -1 unrunnable, 0 runnable, &gt;0 stopped */
    long counter; //时间片
    //counter的计算不是单纯的累加，需要下面这个优先级这个参数参与
    long priority;//优先级
    long signal;//信号
    struct sigaction sigaction[32];//信号位图
    long blocked;//阻塞状态    /* bitmap of masked signals */
/* various fields */
    int exit_code;//退出码
    unsigned long start_code,end_code,end_data,brk,start_stack;
    long pid,father,pgrp,session,leader;
    unsigned short uid,euid,suid;
    unsigned short gid,egid,sgid;
    long alarm;//警告
    long utime,stime,cutime,cstime,start_time;//运行时间
    //utime是用户态运行时间 cutime是内核态运行时间
    unsigned short used_math;
/* file system info */
    int tty;    //是否打开了控制台    /* -1 if no tty, so it must be signed */
    unsigned short umask;
    struct m_inode * pwd;
    struct m_inode * root;
    struct m_inode * executable;
    unsigned long close_on_exec;
    struct file * filp[NR_OPEN];//打开了多少个文件
/* ldt for this task 0 - zero 1 - cs 2 - ds&amp;ss */
    struct desc_struct ldt[3];//ldt包括两个东西，一个是数据段（全局变量静态变量等），另一个是代码段，不过这里面存的都是指针
/* tss for this task */
    struct tss_struct tss;//进程运行过程中CPU需要知道的进程状态标志（段属性、位属性等）
};</code></pre></div><p>&#160; 只要拿到task数组，就等于得到了所有的task结构体，也就掌控了所有的进程；我们平时用的ps命令、用调试器查看所有进程疑似就是这样得到进程列表的！用数组管理task结构体属于早期方法，这种方法的缺点也很明显：由于数组定长，这里只能“同时”运行64个进程，所以后来windows改成了双向链表：进程和线程之间都是通过双向链表连接的，遍历也是通过双向链表，这样对于进程或线程的数量就没有限制了（只要内存足够大）！</p><p>&#160; 2、进程创建好后就需要调度了，核心代码在sched.c的schedule函数中，如下：</p><p>如果时间到点了，设置进程的sigalrm信号；如果进程处于可中断休眠状态，那么把该进程设置为可运行状态；<br />接着找到时间片最大的进程；如果没找到，就根据优先级重新计算进程的时间片，公式很简单：counter = counter/2 + priority<br />最后切换到目标进程执行</p><div class="codebox"><pre class="vscroll"><code> // 时间片分配
void schedule(void)
{
    int i,next,c;
    struct task_struct ** p;//双重指针，指向task结构体数组

/* check alarm, wake up any interruptible tasks that have got a signal */

    for(p = &amp;LAST_TASK ; p &gt; &amp;FIRST_TASK ; --p)//通过task结构体数组从后往前遍历每个task
        if (*p) {//在哪个jiffies发生警告?
            if ((*p)-&gt;alarm &amp;&amp; (*p)-&gt;alarm &lt; jiffies) {//alarm存在，并且已经到点
                    (*p)-&gt;signal |= (1&lt;&lt;(SIGALRM-1));//新增一个警告信号量
                    (*p)-&gt;alarm = 0;//警告清空
                }
                //如果该进程为可中断睡眠状态 则如果该进程有非屏蔽信号出现就将该进程的状态设置为running
            if (((*p)-&gt;signal //有singal
                    &amp; ~(_BLOCKABLE &amp; (*p)-&gt;blocked)) &amp;&amp; //并且非阻塞
                        (*p)-&gt;state==TASK_INTERRUPTIBLE) //并且状态是可中断的
                (*p)-&gt;state=TASK_RUNNING;
        }

/* this is the scheduler proper: */
    // 以下思路，循环task列表 根据counter大小决定进程切换
    while (1) {
        c = -1;
        next = 0;
        i = NR_TASKS;
        p = &amp;task[NR_TASKS];//从最后一个任务开始循环
        while (--i) {
            if (!*--p)//task结构体是空，继续循环
                continue;
            //task结构体不为空，说明有进程
            if ((*p)-&gt;state == TASK_RUNNING &amp;&amp; (*p)-&gt;counter &gt; c)//找出c最大的task
                c = (*p)-&gt;counter, next = i;
        }
        if (c) break;//如果c找到了，就终结循环；如果为0，说明所有进程的时间片都用光了
        //如果没有找到最大时间片的进程，就根据优先级进行时间片的重新分配
        for(p = &amp;LAST_TASK ; p &gt; &amp;FIRST_TASK ; --p)
            if (*p)//这里很关键，在低版本内核中，是进行优先级时间片轮转分配，这里搞清楚了优先级和时间片的关系
            //counter = counter/2 + priority
                (*p)-&gt;counter = ((*p)-&gt;counter &gt;&gt; 1) +
                        (*p)-&gt;priority;
    }
    //切换到下一个进程 这个功能使用宏定义完成的
    switch_to(next);
}</code></pre></div><p>&#160; &#160;任务切换前面的代码容易理解，最后一个switch_to的代码如下：首先声明了一个_tmp的结构，这个结构里面包括两个long型，32位机里面long占32位，声明这个结构主要与ljmp这个长跳指令有关（任务跳转就是通过ljmp指令实现的），这个指令有两个参数，一个参数是段选择符，另一个是偏移地址，所以这个_tmp就是保存这两个参数。再比较任务n是不是当前任务，如果不是则跳转到标号1，否则交互ecx和current的内容，交换后的结果为ecx指向当前进程，current指向要切换过去的新进程；最后执行长跳，%0代表输出输入寄存器列表中使用的第一个寄存器，即&quot;m&quot;(*&amp;__tmp.a)，这个寄存器保存了*&amp;__tmp.a，而_tmp.a存放的是32位偏移，_tmp.b存放的是新任务的tss段选择符，长跳到段选择符会造成任务切换；</p><div class="codebox"><pre><code>/*
 *    switch_to(n) should switch tasks to task nr n, first
 * checking that n isn&#039;t the current task, in which case it does nothing.
 * This also clears the TS-flag if the task we switched to has used
 * tha math co-processor latest.
 */
// 进程切换是用汇编宏定义实现的
//1. 将需要切换的进程赋值给当前进程的指针
//2. 将进程的上下文（TSS和当前堆栈中的信息）切换
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__(&quot;cmpl %%ecx,_current\n\t&quot; \
    &quot;je 1f\n\t&quot; \
    &quot;movw %%dx,%1\n\t&quot; \
    &quot;xchgl %%ecx,_current\n\t&quot; \
    &quot;ljmp %0\n\t&quot; \
    &quot;cmpl %%ecx,_last_task_used_math\n\t&quot; \
    &quot;jne 1f\n\t&quot; \
    &quot;clts\n&quot; \
    &quot;1:&quot; \
    ::&quot;m&quot; (*&amp;__tmp.a),&quot;m&quot; (*&amp;__tmp.b), \
    &quot;d&quot; (_TSS(n)),&quot;c&quot; ((long) task[n])); \
}</code></pre></div><p>这里的切换是通过TSS实现的，这也是x86硬件提供的切换方式，图示如下：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202111/2052730-20211123143336331-1615117063.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;3、进程（为了便于辨识，这里叫A进程）通过schedule开始执行后，不太可能一直运行，中途可能需要等待，比如等待需要某些资源，这时就需要主动把cpu让出去，让其他task执行，避免自己“占着茅坑不拉屎”，这时就需要让进程sleep了，0.11版本的linux是这么干的: 最核心的就是把task结构体中的state改成TASK_UNINTERRUPTIBLE后重新调用schedule函数运行其他任务；由于本任务的状态已经不是TASK_RUNNING了，所以schedule函数不会跳转到当前task执行！注意：调用schedule函数后，如果有时间片高的进程B，会通过ljmp跳转到B进程执行，所以A进程的schedule此时时不返回的，导致下面的if(tmp)代码是不执行的！直到其他某个进程比如C进程调用wake_up函数，把A进程的状态改成runable，C进程再调用schedule函数，A进程才可能继续执行后续的if(tmp)代码！</p><div class="codebox"><pre class="vscroll"><code>// 把当前任务置为不可中断的等待状态，并让睡眠队列指针指向当前任务。
// 只有明确的唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。函数参数P是等待
// 任务队列头指针。指针是含有一个变量地址的变量。这里参数p使用了指针的指针形式&#039;**p&#039;,这是因为
// C函数参数只能传值，没有直接的方式让被调用函数改变调用该函数程序中变量的值。但是指针&#039;*p&#039;
// 指向的目标(这里是任务结构)会改变，因此为了能修改调用该函数程序中原来就是指针的变量的值，
// 就需要传递指针&#039;*p&#039;的指针，即&#039;**p&#039;.
void sleep_on(struct task_struct **p)
{
    struct task_struct *tmp;

    // 若指针无效，则退出。(指针所指向的对象可以是NULL，但指针本身不应该为0).另外，如果
    // 当前任务是任务0，则死机。因为任务0的运行不依赖自己的状态，所以内核代码把任务0置为
    // 睡眠状态毫无意义。
    if (!p)
        return;
    if (current == &amp;(init_task.task))
        panic(&quot;task[0] trying to sleep&quot;);
    // 让tmp指向已经在等待队列上的任务(如果有的话)，例如inode-&gt;i_wait.并且将睡眠队列头的
    // 等等指针指向当前任务。这样就把当前任务插入到了*p的等待队列中。然后将当前任务置为
    // 不可中断的等待状态，并执行重新调度。
    tmp = *p;
    *p = current;
    current-&gt;state = TASK_UNINTERRUPTIBLE;
    schedule();
    // 只有当这个等待任务被唤醒时，调度程序才又返回到这里，表示本进程已被明确的唤醒(就
    // 续态)。既然大家都在等待同样的资源，那么在资源可用时，就有必要唤醒所有等待该该资源
    // 的进程。该函数嵌套调用，也会嵌套唤醒所有等待该资源的进程。这里嵌套调用是指一个
    // 进程调用了sleep_on()后就会在该函数中被切换掉，控制权呗转移到其他进程中。此时若有
    // 进程也需要使用同一资源，那么也会使用同一个等待队列头指针作为参数调用sleep_on()函数，
    // 并且也会陷入该函数而不会返回。只有当内核某处代码以队列头指针作为参数wake_up了队列，
    // 那么当系统切换去执行头指针所指的进程A时，该进程才会继续执行下面的代码，把队列后一个
    // 进程B置位就绪状态(唤醒)。而当轮到B进程执行时，它也才可能继续执行下面的代码。若它
    // 后面还有等待的进程C，那它也会把C唤醒等。在这前面还应该添加一行：*p = tmp.
    if (tmp)                    // 若在其前还有存在的等待的任务，则也将其置为就绪状态(唤醒).
        tmp-&gt;state=0;
}</code></pre></div><p>&#160; 上述整个过程图示如下：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202111/2052730-20211123184601352-1754706366.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 唤醒进程的代码也很简单，如下：核心也是把task的state改成0，也就是runable！注意：这里把*p=NULL是为啥了？ 唤醒进程后，进程终于可以从sleep_on函数中的schedule()下一行代码开始运行，此时会通过tmp-&gt;state=0把状态改成runable，所以如果这个进程下次再被sleep时，wake_up这里不需要再设置状态了！</p><div class="codebox"><pre><code>void wake_up(struct task_struct **p)
{
    if (p &amp;&amp; *p) {
        (**p).state=0;
        *p=NULL;
    }
}</code></pre></div><p>&#160; &#160;4、进程的代码运行完毕，用户也拿到了想要的结果，进程就可以销毁了；销毁进程的入口在kernel/exit.c/do_exit()函数里，流程也不复杂：</p><p>释放ldt占用的内存<br />如果是某个进程的父进程，更子进程的新父进程为1号进程<br />关闭文件<br />关闭终端、清空协处理器<br />给父进程发signal<br />重新调度</p><div class="codebox"><pre class="vscroll"><code>int do_exit(long code)
{
    int i;
    //释放内存页
    free_page_tables(get_base(current-&gt;ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current-&gt;ldt[2]),get_limit(0x17));
    //current-&gt;pid就是当前需要关闭的进程
    for (i=0 ; i&lt;NR_TASKS ; i++)
        if (task[i] &amp;&amp; task[i]-&gt;father == current-&gt;pid) {//如果当前进程是某个进程的父进程
            task[i]-&gt;father = 1;//就让1号进程作为新的父进程
            if (task[i]-&gt;state == TASK_ZOMBIE)//如果是僵死状态
                /* assumption task[1] is always init */
                (void) send_sig(SIGCHLD, task[1], 1);//给父进程发送SIGCHLD
        }
    for (i=0 ; i&lt;NR_OPEN ; i++)//每个进程能打开的最大文件数NR_OPEN=20
        if (current-&gt;filp[i])
            sys_close(i);//关闭文件
    iput(current-&gt;pwd);
    current-&gt;pwd=NULL;
    iput(current-&gt;root);
    current-&gt;root=NULL;
    iput(current-&gt;executable);
    current-&gt;executable=NULL;
    if (current-&gt;leader &amp;&amp; current-&gt;tty &gt;= 0)
        tty_table[current-&gt;tty].pgrp = 0;//清空终端
    if (last_task_used_math == current)
        last_task_used_math = NULL;//清空协处理器
    if (current-&gt;leader)
        kill_session();//清空session
    current-&gt;state = TASK_ZOMBIE;//设为僵死状态
    current-&gt;exit_code = code;
    tell_father(current-&gt;father);
    schedule();
    return (-1);    /* just to suppress warnings */
}</code></pre></div><p>&#160; 在执行do_exit方法的时候，间接调用了一些重要的函数，如下：</p><p>&#160; （1）release函数：释放task结构体本身占用的内存，并重新调度</p><div class="codebox"><pre><code>void release(struct task_struct * p)
{
    int i;

    if (!p)
        return;
    for (i=1 ; i&lt;NR_TASKS ; i++)//在task[]中进行遍历
        if (task[i]==p) {
            task[i]=NULL;
            free_page((long)p);//释放内存页
            schedule();//重新进行进程调度
            return;
        }
    panic(&quot;trying to release non-existent task&quot;);
}</code></pre></div><p>&#160; （2）给指定的进程发送信号，本质就是通过task结构体给对方进程的signal字段增加一个值！为了确保安全，给对方发信号需要具备以下三个条件之一：</p><p>权限不为0<br />euid确实是当前进程的<br />系统超级用户</p><div class="codebox"><pre><code>static inline int send_sig(long sig,struct task_struct * p,int priv)
{
    if (!p || sig&lt;1 || sig&gt;32)
        return -EINVAL;
    if (priv //要么权限不为0
        || (current-&gt;euid==p-&gt;euid) //要么euid相等（当前进程使用者）
            || suser()) //要么是超级用户才能给另一个进程发信号，这里可以确保安全，避免接收到恶意信号
        p-&gt;signal |= (1&lt;&lt;(sig-1));
    else
        return -EPERM;
    return 0;
}</code></pre></div><p>&#160; （3）关闭进程间对话的session：居然也是给task结构体的signal字段增加一个值！</p><div class="codebox"><pre><code>//关闭session
static void kill_session(void)
{
    struct task_struct **p = NR_TASKS + task;//指向最后一个task结构体
    while (--p &gt; &amp;FIRST_TASK) {//从最后一个开始扫描（不包括0进程）
        if (*p &amp;&amp; (*p)-&gt;session == current-&gt;session)//确认确实是当前会话
            (*p)-&gt;signal |= 1&lt;&lt;(SIGHUP-1);
    }
}</code></pre></div><p>&#160; （4）通知被销毁进程的父进程：通过遍历task结构体数组找到父进程的task结构体，然后增加signal字段的sigchld值！（本人调试x音的时候ida经常会收到这个消息，只要点击确认x音就直接退出）</p><div class="codebox"><pre><code>static void tell_father(int pid)
{
    int i;
    if (pid)
        for (i=0;i&lt;NR_TASKS;i++) {
            if (!task[i])
                continue;
            if (task[i]-&gt;pid != pid)
                continue;
            task[i]-&gt;signal |= (1&lt;&lt;(SIGCHLD-1));//找到父亲发送SIGCHLD信号
            return;
        }
/* if we don&#039;t find any fathers, we just release ourselves */
/* This is not really OK. Must change it to make father 1 */
    printk(&quot;BAD BAD - no father found\n\r&quot;);
    release(current);//释放子进程
}</code></pre></div><p>&#160; 总结：这里的tell_father、kill_session都是通过发送signal实现的；发送signal的方式也很简单：直接找到对方的task结构体，通过“或”逻辑运算增加信号量！</p><p>&#160; &#160;（5）还有一个“挂羊头、卖狗肉”的方法：sys_kill如下：名字叫sys_kill，实际上是在给目标task结构体发信号！linux的shell中kill命令就是用这个函数实现的！</p><div class="codebox"><pre><code>// 系统调用 向任何进程 发送任何信号（类比shell中的kill命令也是发送信号的意思）
int sys_kill(int pid,int sig)
{
    struct task_struct **p = NR_TASKS + task;//指向最后
    int err, retval = 0;

    if (!pid) while (--p &gt; &amp;FIRST_TASK) {
        if (*p &amp;&amp; (*p)-&gt;pgrp == current-&gt;pid) //如果pid=0,就给当前进程所在的进程组发信号
            if (err=send_sig(sig,*p,1))
                retval = err;
    } else if (pid&gt;0) while (--p &gt; &amp;FIRST_TASK) {//pid&gt;0给对应进程发送信号
        if (*p &amp;&amp; (*p)-&gt;pid == pid) 
            if (err=send_sig(sig,*p,0))
                retval = err;
    } else if (pid == -1) while (--p &gt; &amp;FIRST_TASK)//pid=-1给任何进程发送
        if (err = send_sig(sig,*p,0))
            retval = err;
    else while (--p &gt; &amp;FIRST_TASK)//pid&lt;-1 给进程组号为-pid的进程组发送信息
        if (*p &amp;&amp; (*p)-&gt;pgrp == -pid)
            if (err = send_sig(sig,*p,0))
                retval = err;
    return retval;
}</code></pre></div><p>&#160; 5、父进程创建子进程时，有时候需要等待子进程执行完毕，需要调用sys_waitpid函数阻塞父进程自己，如下：如果子进程是僵死状态，就把子进程运行的时间叠加到父进程，然后释放子进程的task结构体！如果子进程还在运行，父进程通过schedule让出cpu，把自己阻塞；下次被唤醒后再次检查子进程是否给自己发送了SIGCHLD信号；如果没收到，重复检查的过程，直到子进程庄涛变为僵死后返回继续执行后续的代码！</p><div class="codebox"><pre class="vscroll"><code>int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
    int flag, code;
    struct task_struct ** p;

    verify_area(stat_addr,4);//验证区域是否可以用
repeat:
    flag=0;
    for(p = &amp;LAST_TASK ; p &gt; &amp;FIRST_TASK ; --p) {
        if (!*p || *p == current)
            continue;
        if ((*p)-&gt;father != current-&gt;pid)
            continue;
        if (pid&gt;0) {
            if ((*p)-&gt;pid != pid)
                continue;
        } else if (!pid) {
            if ((*p)-&gt;pgrp != current-&gt;pgrp)
                continue;
        } else if (pid != -1) {
            if ((*p)-&gt;pgrp != -pid)
                continue;
        }
        switch ((*p)-&gt;state) {
            case TASK_STOPPED://子进程是stop状态
                if (!(options &amp; WUNTRACED))
                    continue;
                put_fs_long(0x7f,stat_addr);
                return (*p)-&gt;pid;
            case TASK_ZOMBIE://子进程是僵死状态：把子进程消耗的时间叠加到父进程，并释放子进程的结构体
                current-&gt;cutime += (*p)-&gt;utime;
                current-&gt;cstime += (*p)-&gt;stime;
                flag = (*p)-&gt;pid;
                code = (*p)-&gt;exit_code;
                release(*p);
                put_fs_long(code,stat_addr);
                return flag;
            default:
                flag=1;//子进程还在运行，设置flag为1，好让下面的代码执行
                continue;
        }
    }
    if (flag) {//说明子进程状态不是stop或zombie，父进程需要阻塞等待，最直接的办法就是让出cpu
        if (options &amp; WNOHANG)
            return 0;
        current-&gt;state=TASK_INTERRUPTIBLE;//设置程可种段的
        schedule();//父进程阻塞，让出cpu
        //当父进程再次被唤醒后，检查一下是否收到了子进程结束的通知；如果没有，再次从repeate开始执行
        if (!(current-&gt;signal &amp;= ~(1&lt;&lt;(SIGCHLD-1))))
            goto repeat;
        else
            return -EINTR;
    }
    return -ECHILD;
}</code></pre></div><p>&#160; &#160;这么一圈代码解读下来，个人觉得的需要总结的一些要点：</p><p>所谓进程，本质上就是task结构体；结构体包含了很多字段属性，用来描述进程的方方面面（借鉴了面向对象的思想）；<br />对于进程的各种操作，本质上就是修改task结构体的属性，通过这些属性的逻辑组合完成各种复杂的功能；这里有点像汽车：汽车本质上也是由螺丝钉、齿轮、轴承等基础零配件构成的，但这些零配件通过一定的逻辑关系结合，就实现了复杂的功能！<br />找到task数组就等于找到所有进程的task结构体；<br />&#160; &#160;为了便于记忆和理解，整理了一些要点：</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202111/2052730-20211124214730341-2054005611.png" alt="FluxBB bbcode 测试" /></span> </p><br /><p>&#160; &#160;后续解读linux源码的时候，会发现大量的struct，每个struct又有很多字段构成，了解清楚每个字段的含义才能真正理解操作系统的各个细节，这里简单总结一下struct内部各个变量的类型：</p><p>指针：也就是地址<br />计数/计量的，比如size、length、index、count、height、amount、sequenceNo等<br />有应用业务意义的值：id、name等<br />标记位/控制位：flags等</p><br /><p>参考：</p><p>1、源码下载：https://mirrors.edge.kernel.org/pub/linux/kernel/</p><p>&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; <a href="https://github.com/karottc/linux-0.11" rel="nofollow">https://github.com/karottc/linux-0.11</a><br />2、https://www.bilibili.com/video/BV1tQ4y1d7mo?p=1&#160; 操作系统体系结构<br />3、https://www.bilibili.com/video/BV1VJ41157wq?spm_id_from=333.999.0.0&#160; linux操作系统-构建自己的内核<br />4、https://blog.csdn.net/heiworld/article/details/25397155&#160; 对linux 0.11版本中switch_to()的理解<br />5、https://www.rutk1t0r.org/2016/12/23/Linux%E5%86%85%E6%A0%B80-11%E5%AE%8C%E5%85%A8%E6%B3%A8%E9%87%8A-%E5%85%B3%E4%BA%8E%E4%BB%BB%E5%8A%A1%E7%9D%A1%E7%9C%A0%E5%92%8C%E5%94%A4%E9%86%92%E7%9A%84%E7%90%86%E8%A7%A3/&#160; Linux内核0.11完全注释 关于任务睡眠和唤醒的理解<br />6、https://blog.csdn.net/u012351051/article/details/79646843&#160; linux-0.11/init/main.c流程分析</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sun, 09 Oct 2022 02:44:32 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=457#p457</guid>
		</item>
	</channel>
</rss>
