百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Linux内核是如何初始化操作系统,并运行第一个程序的

liebian365 2024-11-27 17:08 2 浏览 0 评论

problem:

Linux内核是如何初始化操作系统,并开始运行第一个程序呢?

我们都知道,系统启动过程为:bootsect.s —>setup.s —>head.s。姑且不去讨论这些汇编源程序的功能,假设操作系统的pc指针已经运行到了head.s 处的部分代码,这里做下仔细的研究。
目标代码如下(linux/boot/head.s):

17:startup_32:
18:    movl $0x10,%eax
......
48:    call check_x87
49:    jmp after_page_tables #跳转到135行
......
135:after_page_tables:
136:    pushl $0
137:    pushl $0
138:    pushl $0
139:    pushl $L6
140:    pushl $_main
141:    jmp setup_paging #跳转到198行
142:L6:
143:    jmp L6;
......
198:setup_paging:
199:    movl $1024*5,%ecx
......
217:    movl %eax,%cr0
218:    ret

忽略其他细节问题,当head程序执行到第49行时,将跳转到135行代码出运行。在第135行代码处,便是head.s调用init中main函数的核心。回顾c函数与汇编之间相互调用的知识可知,内核栈中存在:

当代码继续跳转后,会进入198行代码,直到程序运行至ret,调用返回。此时,内核栈中指向main函数将弹栈,pc指正自动指向main函数地址入口出,从而开始执行main函数。其次,传入main函数中的三个参数均为0(初始化main函数,无需传参,因此这里默认为0)。

更多Linux内核视频教程文档资料免费领取后台私信【内核】自行获取。

内核学习网站:

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

目标代码如下(linux/init/main.c):

void main(void){
    .......
    move_to_user_mode();//移到用户模式下执行  什么叫做移动到用户模式下?未解 o(︶︿︶)o 
    if(!fork()){
        init();
    }
    for(;;)
        __asm__("int $0x80"::"a"(__NR_pause):"ax");
}

main函数是系统创建的第一个进程,作为第一个进程,它有自己的堆栈段,数据段,代码段等。在进入用户模式下后,便调用了fork函数来创建新的子进程,创建完进程后,立马进入暂停状态。而第二个进程将在init()函数下继续运行。init()函数中,便是用来调用linux的shell脚本程序,从而可以与用户进行交互。那么问题来了,fork是如何做到进程的创建的呢?系统如何让两个进程或多个进程并发的执行呢?进程实现的原理是什么?

进程实现的原理

一个最简单的想法,或者说是最初的想法便是让pc指针分时地进行地址跳转,这的确是最核心最正确的解决办法,可具体该怎样实现?在用户状态下,用户进行函数调用,是需要把函数参数,函数地址,局部变量等进行压栈的。而函数调用其实本事就是个让pc指针变化的事。请看图:

A函数运行一段时间后,必然会转入B函数,且调用Yield函数,在进行地址跳转之前,先将该程序下两个地址进行压栈。而一旦调Yield函数,做的操作便是,将A程序的用户栈和将被调用的B程序的用户栈进行切换。Yield的具体实现也正是这么做的,交换了两个线程控制块。由于现在esp指向了C函数的入口地址,操作系统便开始执行C函数了,同样一旦运行到D函数,再次调用Yield,进行用户栈切换,这时,当前esp=1000,当Yield函数调用结束,即ret返回,自动弹栈。PC=204,返回A程序执行。至此,两个线程便能交替的执行任务了。

注意:Liunx内核中,并没有实现线程的机制,但为了阐述进程的实现原理,理解线程的实现原理是必要的。也正如我们在操作系统学到的那样:

线程是独立调度的基本单位,用户级线程没有进入系统内核,调用计算机资源,仅仅在用户状态下运行即可。

进程之所以是资源分配的基本单位,除了要完成用户态栈的切换,还需要内核栈的切换,其次进程的创建fork函数是系统调用,它将通过中断进入系统,调度计算机的资源。

所以,通过上述总结,进程调度的实现原理就是在线程调度的基础上,加上了内核栈与用户栈的关联步骤,并且在内核态进行两个栈的切换,即PCB的切换。来分析代码咯!

目标代码(linux/kernel/sys_call.s):

_system_call:  
cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在eax 中置-1 并退出。  
ja bad_sys_call  
push %ds # 保存原段寄存器值。  
push %es  
push %fs  
pushl %edx # ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。  
pushl %ecx # push %ebx,%ecx,%edx as parameters  
pushl %ebx # to the system call  
movl $0x10,%edx # set up ds,es to kernel space  
mov %dx,%ds # ds,es 指向内核数据段(全局描述符表中数据段描述符)。  
mov %dx,%es  
movl $0x17,%edx # fs points to local data space  
mov %dx,%fs # fs 指向局部数据段(局部描述符表中数据段描述符)。  
# 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。  
# 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个  
# 系统调用C 处理函数的地址数组表。  
call _sys_call_table(,%eax,4)  
pushl %eax # 把系统调用号入栈。  
movl _current,%eax # 取当前任务(进程)数据结构地址??eax。  
# 下面97-100 行查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。  
# 如果该任务在就绪状态但counter[??]值等于0,则也去执行调度程序。  
cmpl $0,state(%eax) # state  
jne reschedule  
cmpl $0,counter(%eax) # counter  
je reschedule  

在系统调用这章,详细讨论了sys_call_table的作用。今天我们便要研究透这段代码(这里插段自己学习的经验总结,遇到源码看不懂的问题,没关系,遇到知识瓶颈再正常不过了,不要花太多时间硬啃,可以放一放。随着你每天日积月累的总结,当时看不懂不代表现在看不懂,当再次回顾代码时,可以把新学的内容套用上,便能慢慢研究透源代码了)。

代码4-9行,进行了一系列的压栈操作,这便是所谓的对当前操作系统状态进行一个保存,记录当前进程运行的信息。内核栈如下:

小伙伴可能要问了,4-9行明明没有ss:sp、EFLAGS、ret的内容啊,怎么会出现在内核栈呢。还记得前文所述的进程与线程的区别嘛?进程是通过中断来完成创建的,因此在调用fork函数时,便自动产生软中断,一旦中断发生,便由硬件自动返程ss:sp、EFLAGS、ret的压栈。

仔细看上图,ss:sp指向的便是进程在用户状态下的用户栈地址。这也就实现了进程的内核栈与用户栈的关联。

注意:

  • ds,es,fs为当前进程的数据段。
  • edx,ecx,ebx是系统调用规定的必要参数,ebx为系统调用的第一个参数,以此类推,edx为第三个参数。
  • 代码10-14进行内核态的切换,寄存器指向内核数据段切换到内核态后,便可以执行sys_fork函数了。
  • 同样在该目标代码中出现:
.align 2
_sys_fork:
    call _find_empty_process
    testl %eax,%eax
    js lf
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process
    addl $20,%esp
l:  ret

首先调用find-empty-process函数,具体代码不再阐述了,可以翻看linux内核剖析。具体位置在linux/kernel/fork.c下,该函数的主要作用为:last_pid与当前系统内的进程号进行比较,如果当前进程号=last_pid且当前任务处于运行状态,则last_pid++;直到找到一个可用的进程号,然后返回,此时eax存放的内容便是last_pid。

函数返回可能会发生两种情况:

当eax 为负数时,直接执行ret函数,sys_fork函数调用返回

当eax为正数时,把寄存器gs、esi、edi、ebp、eax的内容压栈,并调用copy_process函数。

目标代码(linux/kernel/fork.c):

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;
    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
    task[nr] = p;
    *p = *current;    /* NOTE! this doesn't copy the supervisor stack */
    p->state = TASK_UNINTERRUPTIBLE; //进程在建立过程中为防止被调度,设置为不可中断等待。
    p->pid = last_pid; //进程的id
    p->father = current->pid; //进程的父进程id
    p->counter = p->priority; //进程的优先级,这个只能通过sys_nice来修改。
    ......
    p->tss.eax=0            //请思考作用
    ......
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
    p->state = TASK_RUNNING;    /* do this last, just in case 到此进程准备完毕,可以运行,则状态修改为就绪 */

    return last_pid; //返回新建子进程的ID
}

代码请注意:p->tss.eax=0,作用是什么?

copy_process函数所传入的参数对应为当前系统内核栈的参数,即nr=(%eax),ebp=(%ebp)….依此类推ss=(%ss)。该函数的主要作用是:

创建新的PCB,并且给该进程控制块分配新的物理内存

初始化PCB的内容,并且将父进程的内核栈信息全部复制给子进程的内核栈。

函数成功调用后,最终返回系统进程调用号。至此,子进程如果不出意外便算创建成功了。函数返回后,便又回到了sys_call的代码段,我们继续分析接下来的代码。

需要了解的是,子进程虽然被创建了,但目前处于就绪状态,它只是存在于内存的某处,并没有开始执行调度。因此,此时copy-process返回的便是子进程的pid,显然eax=pid且 !0。值得注意的是,现在这个状态还是父亲进程的运行态。它将执行一系列符合条件的进程调度操作,如果幸运的话,可能也不会执行调度,直接弹栈,中断返回。

目标代码如下(linux/init/main.c):

void main(void){
        .......
        move_to_user_mode();//移到用户模式下执行  什么叫做移动到用户模式下?未解 o(︶︿︶)o 
        if(!fork()){
            init();
        }
        for(;;)
            __asm__("int $0x80"::"a"(__NR_pause):"ax");
    }

还记得操作系统初始化的这段代码嘛,这里便有一个fork调用,接着上面的分析,调用fork后,附进程的pid!=0,所以将跳过if语句往下执行,这里让进程通过sys_pause强制暂停了。再次进入中断后,操作系统开始寻找内核里存在的就绪进程,这时候发现有一个新的就绪态的子进程在默默地等待,因此通过jne reschedule执行函数的调度。reschedule将接着继续调用schedule。

目标代码(linux/kernel/sched.c):

void schedule(void){
    int i,next,c;
    .......
    switch_to(next);
}

进入调度函数后,先忽略前述的某些优先级调度算法,直接转入switch_to函数,你会发现它是通过建立映象来完成内核栈的切换的。而内核栈切换是进程调度的核心的核心,switch_to代码的工作原理是通过TR寄存器来找到当前的tss段,当寄存器存放的内容发生改变时,便会导致TR指向的tss的内容写入CPU的所有寄存器当中,而将原来的寄存器的内容保存在原来的tss中。

代码分析:

将任务n的tss描述赋值给edx寄存器

将edx寄存器的第16位内容,传给临时变量tmp.b

执行长跳转ljmp,ljmp可分为两步:

将寄存器的内容写入当前进程的tss当中去,并且把原tss的描述符传给临时变量转给tmp.a,即给cpu寄存器拍了个照,留存下来。

将tmp.b指向的新tss的内容全部映射到寄存器中,从而完成切换。

switch_to(n)一旦完成切换,内核栈的内容便是子进程的内容了。还记得fork出p->tss.eax为什么等于0了嘛,此时切换的子进程tss.eax=0,那就意味着swicth_to后,cpu寄存器的eax=0,那么等中断返回后,!fork()条件就变成了真!还记得子进程是复制了父进程内核栈的信息了嘛,所以当子进程中断返回后,不就是返回到if后面一条语句执行吗!so,子进程便能在操作系统开始它自己的工作了。。。o(∩_∩)o

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: