《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——2.9 初始化进程0

简介: 本节书摘来自华章计算机《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》一书中的第2章,第2.9节,作者:新设计团队著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.9 初始化进程0

进程0是Linux操作系统中运行的第一个进程,也是Linux操作系统父子进程创建机制的第一个父进程。下面讲解的内容对进程0能够在主机中正常运算的影响最为重要和深远,主要包含如下三方面的内容。
1)系统先初始化进程0。进程0管理结构task_struct的母本(init_task = {INIT_TASK,})已经在代码设计阶段事先设计好了,但这并不代表进程0已经可用了,还要将进程0的task_struct中的LDT、TSS与GDT相挂接,并对GDT、task[64]以及与进程调度相关的寄存器进行初始化设置。
2)Linux 0.11作为一个现代操作系统,其最重要的标志就是能够支持多进程轮流执行,这要求进程具备参与多进程轮询的能力。系统这里对时钟中断进行设置,以便在进程0运行后,为进程0以及后续由它直接、间接创建出来的进程能够参与轮转奠定基础。
3)进程0要具备处理系统调用的能力。每个进程在运算时都可能需要与内核进行交互,而交互的端口就是系统调用程序。系统通过函数set_system_gate将system_call与IDT相挂接,这样进程0就具备了处理系统调用的能力了。这个system_call就是系统调用的总入口。
进程0只有具备了以上三种能力才能保证将来在主机中正常地运行,并将这些能力遗传给后续建立的进程。
这三点的实现都是在sched_init()函数中实现的,具体代码如下:

//代码路径:init/main.c:
void main(void)
{
    …
    sched_init();
    …
}
//代码路径:kernel/sched.c:
    …
#define LATCH (1193180/HZ)        //每个时间片的振荡次数
    …
union task_union {            // task_struct与内核栈的共用体
    struct task_struct task;
    char stack[PAGE_SIZE];    // PAGE_SIZE是4 KB
};
static union task_union init_task= {INIT_TASK,};//进程0的task_struct
    …
//初始化进程槽task[NR_TASKS]的第一项为进程0,即task[0]为进程0占用
struct task_struct * task[NR_TASKS]= {&(init_task.task), };
    …

void sched_init(void)
{
    int i;
    struct desc_struct * p;

    if (sizeof(struct sigaction) != 16)
         panic("Struct sigaction MUST be 16 bytes");
    set_tss_desc(gdt + FIRST_TSS_ENTRY,&(init_task.task.tss));//设置TSS0
    set_ldt_desc(gdt + FIRST_LDT_ENTRY,&(init_task.task.ldt));//设置LDT0
    p= gdt + 2+FIRST_TSS_ENTRY;    //从GDT的6项,即TSS1开始向上全部清零,并且将进程槽从 
    for(i=1;i<NR_TASKS;i++) {    //1往后的项清空。0项为进程0所用
         task[i]= NULL;
         p->a=p->b=0;
         p++;
         p->a=p->b=0;
         p++;
    }
/* Clear NT, so that we won't have troubles with that later on */
    __asm__("pushfl;andl $0xffffbfff,(%esp);popfl");
    ltr(0);    //重要!将TSS挂接到TR寄存器
    lldt(0);    //重要!将LDT挂接到LDTR寄存器
    outb_p(0x36,0x43);    /* binary, mode 3, LSB/MSB, ch 0 *///设置定时器
    outb_p(LATCH & 0xff , 0x40);    /* LSB */    //每10毫秒一次时钟中断
    outb(LATCH >> 8 , 0x40);    /* MSB */
    set_intr_gate(0x20,&timer_interrupt);    //重要!设置时钟中断,进程调度的基础
    outb(inb_p(0x21)&~0x01,0x21);        //允许时钟中断
    set_system_gate(0x80,&system_call);        //重要!设置系统调用总入口
}

//代码路径:include\linux\sched.h://         //嵌入汇编参看trap_init的注释
    …
#define FIRST_TSS_ENTRY 4    //参看图2-15中GDT的4项,即TSS0入口
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY + 1)//同上,5项即LDT0入口
#define _TSS(n) ((((unsigned long) n)<<4) + (FIRST_TSS_ENTRY<<3))
#define _LDT(n) ((((unsigned long) n)<<4) + (FIRST_LDT_ENTRY<<3))
#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
    …

//代码路径:include\asm\system.h:
    …
#define set_intr_gate(n,addr) \
    _set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr) \
    _set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)
    …
#define _set_tssldt_desc(n,addr,type) \     //嵌入汇编参看trap_init的注释

__asm__ ("movw $104,%1\n\t" \        //将104,即1101000存入描述符的第1、2字节
          "movw %%ax,%2\n\t" \    //将tss或ldt基地址的低16位存入描述符的第
    //3、4字节
          "rorl $16,%%eax\n\t" \        //循环右移16位,即高、低字互换
          "movb %%al,%3\n\t" \        //将互换完的第1字节,即地址的第3字节存入第5字节
          "movb $" type ",%4\n\t" \    //将0x89或0x82存入第6字节
          "movb $0x00,%5\n\t" \        //将0x00存入第7字节
          "movb %%ah,%6\n\t" \    //将互换完的第2字节,即地址的第4字节存入第8字节
          "rorl $16,%%eax" \    //复原eax    
          ::"a" (addr), "m" (*(n)), "m" (*(n + 2)), "m" (*(n + 4)), \
          "m" (*(n + 5)), "m" (*(n + 6)), "m" (*(n + 7)) \
         //"m" (*(n))是gdt第n项描述符的地址开始的内存单元
         //"m" (*(n + 2)) 是gdt第n项描述符的地址向上第3字节开始的内存单元
         //其余依此类推
          )
//n:gdt的项值,addr:tss或ldt的地址,0x89对应tss,0x82对应ldt
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")

//代码路径:include/linux/sched.h:
    …
struct tss_struct {
    long    back_link;    /* 16 high bits zero */
    long    esp0;
    long    ss0;        /* 16 high bits zero */
    long    esp1;
    long    ss1;        /* 16 high bits zero */
    long    esp2;
    long    ss2;        /* 16 high bits zero */
    long    cr3;
    long    eip;
    long    eflags;
    long    eax,ecx,edx,ebx;
    long    esp;
    long    ebp;
    long    esi;
    long    edi;
    long    es;        /* 16 high bits zero */
    long    cs;        /* 16 high bits zero */
    long    ss;        /* 16 high bits zero */
    long    ds;        /* 16 high bits zero */
    long    fs;        /* 16 high bits zero */
    long    gs;        /* 16 high bits zero */
    long    ldt;        /* 16 high bits zero */
    long    trace_bitmap;    /* bits: trace 0, bitmap 16-31 */
    struct i387_struct i387;
};

struct task_struct {
/* these are hardcoded - don't touch */
    long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    long 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;
    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&ss */
    struct desc_struct ldt[3];
/* tss for this task */
    struct tss_struct tss;
};

/*进程0的task_struct
 *  INIT_TASK is used to set up the first task table, touch at
 * your own risk!. Base=0, limit=0x9ffff (=640kB)
 */
#define INIT_TASK \
/* state etc */    { 0,15,15, \    //就绪态,15个时间片
/* signals */    0,{{},},0, \
/* ec,brk... */    0,0,0,0,0,0, \
/* pid etc.. */    0,-1,0,0,0, \    //进程号0
/* uid etc */    0,0,0,0,0,0, \
/* alarm */    0,0,0,0,0,0, \
/* math */    0, \
/* fs info */    -1,0022,NULL,NULL,NULL,0, \
/* filp */    {NULL,}, \
    { \
         {0,0}, \
/* ldt */    {0x9f,0xc0fa00}, \
         {0x9f,0xc0f200}, \
    }, \
/*tss*/    {0,PAGE_SIZE + (long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
     0,0,0,0,0,0,0,0, \        //eflags的值,决定了cli这类指令只能在0特权级使用
     0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
     _LDT(0),0x80000000, \
         {} \
    }, \
}

2.9.1 初始化进程0
sched_init函数比较难理解的是下面两行:

set_tss_desc(gdt + FIRST_TSS_ENTRY,&(init_task.task.tss));
    set_ldt_desc(gdt + FIRST_LDT_ENTRY,&(init_task.task.ldt));

这两行代码的目的就是要像图2-17表现的那样在GDT中初始化进程0所占的4、5两项,即初始化TSS0和LDT0。

image

另外,要拼出图2-18所示的结构。我们以TSS0为例,参看源代码中的注释,可以绘出图2-19。LDT0类似。

image

对比源代码、注释和图,可以看出,movw $104,%1是将104赋给了段限长15:0的部分;粒度G为0,说明限长就是104字节,而TSS除去struct i387_struct i387后长度正好是104字节。LDT是3×8 =24字节,所以104字节限长够用。TSS的类型是0x89,即二进制的10001001,可以看出movb $" type ",%4在给type赋值1001的同时,顺便将P、DPL、S字段都赋值好了。同理,movb $0x00,%5在给段限长19:16部分赋值0000的同时,顺便将G、D/B、保留、AVL字段都赋值好了。

image

进程0的task_struct是由操作系统设计者事先写好的,就是sched.h中的INIT_TASK(参看上面相关源代码和注释,其结构示意见图2-20),并用INIT_TASK的指针初始化task[64]的0项。

image

sched_init()函数接下来用for循环将task[64]除进程0占用的0项外的其余63项清空,同时将GDT的TSS1、LDT1往上的所有表项清零,效果如图2-21所示。
初始化进程0相关的管理结构的最后一步是非常重要的一步,是将TR寄存器指向TSS0、LDTR寄存器指向LDT0,这样,CPU就能通过TR、LDTR寄存器找到进程0的TSS0、LDT0,也能找到一切和进程0相关的管理信息。

image

2.9.2 设置时钟中断
接下来就对时钟中断进行设置。时钟中断是进程0及其他由它创建的进程轮转的基础。对时钟中断进行设置的过程具体分为如下三个步骤。
1)对支持轮询的8253定时器进行设置。这一步操作如图2-20中的第一步所示,其中LATCH最关键。LATCH是通过一个宏定义的,通过它在sched.c中的定义“#define LATCH (1193180/HZ)”,即系统每10毫秒发生一次时钟中断。
2)设置时钟中断,如图2-22中的第二步所示, timer_interrupt()函数挂接后,在发生时钟中断时,系统就可以通过IDT找到这个服务程序来进行具体的处理。
3)将8259A芯片中与时钟中断相关的屏蔽码打开,时钟中断就可以产生了。从现在开始,时钟中断每1/100秒就产生一次。由于此时处于“关中断”状态,CPU并不响应,但进程0已经具备参与进程轮转的潜能。
2.9.3 设置系统调用总入口
将系统调用处理函数system_call与 int 0x80中断描述符表挂接。system_call是整个操作系统中系统调用软中断的总入口。所有用户程序使用系统调用,产生int 0x80软中断后,操作系统都是通过这个总入口找到具体的系统调用函数。该过程如图2-23所示。

image

image

系统调用函数是操作系统对用户程序的基本支持。在操作系统中,依托硬件提供的特权级对内核进行保护,不允许用户进程直接访问内核代码。但进程有大量的像读盘、创建子进程之类的具体事务处理需要内核代码的支持。为了解决这个矛盾,操作系统的设计者提供了系统调用的解决方案,提供一套系统服务接口。用户进程只要想和内核打交道,就调用这套接口程序,之后,就会立即引发int 0x80软中断,后面的事情就不需要用户程序管了,而是通过另一条执行路线——由CPU对这个中断信号响应,翻转特权级(从用户进程的3特权级翻转到内核的0特权级),通过IDT找到系统调用端口,调用具体的系统调用函数来处理事务,之后,再iret翻转回到进程的3特权级,进程继续执行原来的逻辑,这样矛盾就解决了。

相关文章
|
12月前
|
安全 网络协议 Linux
深入理解Linux内核模块:加载机制、参数传递与实战开发
本文深入解析了Linux内核模块的加载机制、参数传递方式及实战开发技巧。内容涵盖模块基础概念、加载与卸载流程、生命周期管理、参数配置方法,并通过“Hello World”模块和字符设备驱动实例,带领读者逐步掌握模块开发技能。同时,介绍了调试手段、常见问题排查、开发规范及高级特性,如内核线程、模块间通信与性能优化策略。适合希望深入理解Linux内核机制、提升系统编程能力的技术人员阅读与实践。
1000 1
|
监控 Ubuntu Linux
什么Linux,Linux内核及Linux操作系统
上面只是简单的介绍了一下Linux操作系统的几个核心组件,其实Linux的整体架构要复杂的多。单纯从Linux内核的角度,它要管理CPU、内存、网卡、硬盘和输入输出等设备,因此内核本身分为进程调度,内存管理,虚拟文件系统,网络接口等4个核心子系统。
1097 0
|
Web App开发 缓存 Rust
|
Ubuntu 安全 Linux
Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
本地攻击者可以利用上述漏洞,攻击 Ubuntu 22.10、Ubuntu 22.04、Ubuntu 20.04 LTS 发行版,导致拒绝服务(系统崩溃)或执行任意代码。
|
Web App开发 运维 监控
深入探索Linux命令pwdx:揭秘进程工作目录的秘密
`pwdx`命令在Linux中用于显示指定进程的工作目录,基于`/proc`文件系统获取实时信息。简单易用,如`pwdx 1234`显示PID为1234的进程目录。结合`ps`和`pgrep`等命令可扩展使用,如查看所有进程或特定进程(如Firefox)的目录。使用时注意权限、进程ID的有效性和与其他命令的配合。查阅`man pwdx`获取更多帮助。
1136 118
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
1656 5
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
808 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)

热门文章

最新文章