关闭

Linux操作系统

标签: linux内核
335人阅读 评论(0) 收藏 举报
分类:

操作系统第一作用力,是硬件加电那一瞬间启动。它将CS的值0xF000,IP的值为0xFFF0,这样CS:IP的值就指向了0xFFFF0这个地址位置。这是一个纯粹硬件完成的工作,如果这个位置没有可执行代码,那么就什么都不用说了,计算机就此死机。而这个位置,正属于BIOS这个程序段的第一条指令执行之处,随后BIOS加载引导程序到内存,并且在内存中建立了中断向量表。

经历了一系列的加载,进入了main函数。各种硬件设备的初始化,内核手工启动了进程0,然后进程0通过fork复制出进程1,建立了进程管理、内存管理、文件管理、以及进程间通信的基础框架。最后进入了一个等待被应用程序中断的死循环。

理解操作系统本身,理解Linux系统接管硬件以后的事情,如同理解上帝造物造人一样,在这个纷繁复杂的世界中,究竟隐藏着什么瑰丽的进化密码呢。




1),加载BIOS 

当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它。这是因为BIOS中包含了CPU的相关信息、设备启动顺序信息、硬盘信息、内存信息、时钟信息等。在此之后,计算机心里就有谱了,知道应该去读取哪个硬件设备了。计算机中这块芯片里的程序叫做"基本输入输出系统"(Basic Input/Output System),简称为BIOS。

BIOS程序被固化在计算机主板板上的一块很小的ROM芯片里。BIOS启动后,屏幕上会显示显卡的信息,内存的信息,说明BIOS程序在检测显卡,内存。这期间,有一项对启动操作系统至关重要的工作,就是BIOS在内存中建立中断向量表和中断服务程序。





2)、读取MBR主引导记录

硬盘上第0磁道第一个扇区被称为MBR,也就是Master Boot Record,即主引导记录,它的大小是512字节,别看地方不大,可里面却存放了预启动信息、分区表信息。主引导记录只有512个字节,放不了太多东西。它的主要作用是,告诉计算机到硬盘的哪一个位置去找操作系统。主引导记录由三个部分组成:

第1-446字节:调用操作系统的机器码。

第447-510字节:分区表(Partition table)。

第511-512字节:主引导记录签名(0x55和0xAA)。

系统找到BIOS所指定的硬盘的MBR后,就会将其复制到0×7c00地址所在的物理内存中。其实被复制到物理内存的内容就是Boot Loader,而具体到你的电脑,那就是lilo或者grub了。


3)、Boot Loader 

Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好一切准备。


4)、加载内核 

对于Linux操作系统来说,是利用BIOS中的中断服务程序把系统内核加载到内存中的。具体分三次,第一次BIOS中断int 0x19把第一扇区bootsect的内容加载到内存,第二次第三次在bootsect的指挥下,分别把其后的4个扇区和随后的240个扇区的内容加载到内存中。这三个都是由汇编语言写成的程序。bootsect.s、steup.s和system。

通常,我们用C语言编写的程序都是用户应用程序。这类程序的执行都有一个很重要的特征,就是必须在操作系统的平台上执行,也就是说,要由操作系统为应用程序创建进程,并把应用程序的可执行代码从硬盘加载带内存。现在我们讨论的是操作系统个,不是普通的应用程序,这样就出现了一个问题:应用程序由操作系统加载的,那么操作系统是由谁加载呢?

int 0x19对应的中断服务程序的作用是把硬盘第一扇区的程序加载到内存的制定位置,代码是固定的,与操作系统无关。加载完内核boosect以后,就开始执行main函数开始的C语言操作系统内核程序。

C语言的编程经验告诉我们,main函数是程序执行的开始,那么操作系统也不例外。在main函数执行以前,我们需要知道的是,计算机从开机启动,到main函数的执行,所经历的三个步骤。

第一步,启动BIOS,准备实模式下的中断向量表和中断服务程序。

第二步,BIOS从启动盘加载操作系统到内存,加载操作系统程序的工作就是利用第一步准备的中断服务程序来实现的。

第三部,为执行main函数做过渡工作。

接下来,就这样打开了main函数的大门。

/*********************************************************
*                            Linux/init/main.c
* 
*                            (c) 1991 Linux Torvalds
*********************************************************/

#include <unistd.h>
#include <time.h>
#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <linux/system.h>


void main ()
{
    mem_init(main_memory_start,memory_end);     /*内存初始化*/
    trap_init();            /*陷阱门(硬件中断向量)初始化*/
    bik_dev_init();     /*块设备初始化*/
    chr_dev_init();     /*字符设备初始化*/
    tty_init();              /*tty初始化*/
    time_init();           /*设置开机启动时间*/
    sche_init();          /*调试程序初始化*/
    hd_init();              /*硬盘初始化*/
    floppy_init();        /*软驱初始化*/
    sti();                    /*所有的初始化做完了,开启中断*/

    move_to_user_mode;         /*移到用户模式*/

    if(!fork())
    {
      init();              /*下文重点剖析*/
    }


/*在main()函数中已经进行了系统初始化,包括内存管理,IO硬件设备和驱动程序,init()函数运行在任务0第一次创建的子进程(任务1)中,首先对第一个将要执行的程序(shell)的环境进行初始化,然后以登录shell方式加载该程序并执行
*/

    void init()
    {
        int pid,i;

        (void*) open("/dev/tty1",O_RDWR,0);
        (void*) dup(0); /*产生句柄1号-stdout标准输出设备*/
        (void*) dup(0); /*产生句柄2号-stderr标准出错输出设备*/

        if(!(pid=fork()))
        {
            close(0);
            if(open("etc/rc",O_RDONLY,0));   /*若打开文件失败,则退出(lib/_exti.c,10)*/
            _exit(1);/*替换成/bin/sh程序并执行*/
             execve("/bin/sh",argv_rc,envp_rc);    /*若execve()执行失败则退出*/
            _exit(2);
        }
        if(pid<0)
             while(pid!=wait(&i));    /*这是一个空循环*/   

        while(1)
        {
            if(pid=fork())<0)
            {
                printf("Fork failed in init\r\n");
                continue;  
            }

        if(!pid)        /*新的子进程*/
           {
                close(0);
                close(1);
                close(2);
                (void)dup(0);
                (void)dup(0);
                _exit(execve("/bin/sh",argv,envp));
            }

        while(1)
        {
            if(pid==wait(*i)
            break;
        }

        prinf("\n\rchild %d died with code %04x\n\r",pid,i);
        sync();    /*同步操作,刷新缓冲区*/

       }

        _exit(0);  
}


至此,操作系统内核加载完毕。系统将解压后的内核放置在内存之中,并调用start_kernel()函数来启动一系列的初始化函数并初始化各种设备,完成Linux核心环境的建立。至此,Linux内核已经建立起来了,基于Linux的程序应该可以正常运行了。这个部分比较有意思。因为在BIOS阶段,计算机的行为基本上被写死了,程序员可以做的事情并不多;但是,一旦进入操作系统,程序员几乎可以定制所有方面。所以,这个部分与程序员的关系更密切。

操作系统接管硬件以后,首先读入 /boot 目录下的内核文件。



以我的电脑为例,/boot 目录下面大概是这样一些文件:





5)、启动初始化进程

内核文件加载以后,就开始运行第一个程序 /sbin/init,它的作用是初始化系统环境。




由于init是第一个运行的程序,它的进程编号(pid)就是1。其他所有进程都从它衍生,都是它的子进程。


6)、确定运行级别

许多程序需要开机启动。它们在Windows叫做"服务"(service),在Linux就叫做"守护进程"(daemon)。

init进程的一大任务,就是去运行这些开机启动的程序。但是,不同的场合需要启动不同的程序,比如用作服务器时,需要启动Apache,用作桌面就不需要。Linux允许为不同的场合,分配不同的开机启动程序,这就叫做"运行级别"(runlevel)。也就是说,启动时根据"运行级别",确定要运行哪些程序。




Linux预置七种运行级别(0-6)。一般来说,0是关机,1是单用户模式(也就是维护模式),6是重启。运行级别2-5,各个发行版不太一样,对于Debian来说,都是同样的多用户模式(也就是正常模式)。

init进程首先读取文件 /etc/inittab,它是运行级别的设置文件。如果你打开它,可以看到第一行是这样的:

initdefault的值是3,表明系统启动时的运行级别为2。如果需要指定其他级别,可以手动修改这个值。


那么,运行级别2有些什么程序呢,系统怎么知道每个级别应该加载哪些程序呢?每个运行级别在/etc目录下面,都有一个对应的子目录,指定要加载的程序。分贝时rc0.d、rc1.d、rc2.d、rc3.d、rc4.d、rc5.d和rc6.d。目录名中的"rc",表示run command(运行程序),最后的d表示directory(目录)。下面让我们看看 /etc/rc2.d 目录中到底指定了哪些程序。


7)、加载开机启动程序

前面提到,七种预设的"运行级别"各自有一个目录,存放需要开机启动的程序。不难想到,如果多个"运行级别"需要启动同一个程序,那么这个程序的启动脚本,就会在每一个目录里都有一个拷贝。这样会造成管理上的困扰:如果要修改启动脚本,岂不是每个目录都要改一遍?

Linux的解决办法,就是七个 /etc/rcN.d 目录里列出的程序,都设为链接文件,指向另外一个目录 /etc/init.d ,真正的启动脚本都统一放在这个目录中。init进程逐一加载开机启动程序,其实就是运行这个目录里的启动脚本。





8)、用户登录

开机启动程序加载完毕以后,就要让用户登录了。




一般来说,用户的登录方式有三种。这三种情况,都有自己的方式对用户进行认证。

1)命令行登录:init进程调用getty程序(意为get teletype),让用户输入用户名和密码。输入完成后,再调用login程序,核对密码。如果密码正确,就从文件 /etc/passwd 读取该用户指定的shell,然后启动这个shell。

2)ssh登录:这时系统调用sshd程序,然后启动shell。

3)图形界面登录:init进程调用显示管理器,Gnome图形界面对应的显示管理器为gdm(GNOME Display Manager),然后用户输入用户名和密码。如果密码正确,就读取/etc/gdm3/Xsession,启动用户的会话。


9)、进入 login shell

所谓shell,简单说就是命令行界面,让用户可以直接与操作系统对话。用户登录时打开的shell,就叫做login shell。



漫长的启动过程结束了,一切都清静了。。。

操作系统是一个死循环,OS是躺在内存里面等待着被应用程序去调度的代码。内核,就是一个由中断Interrupt驱动的程序代码,这个中断可以是一个系统调用,可以是一个用户进程的异常,也可以是硬件中断的触发。没有任何的外界触发事件,操作系统就是一个死循环,周而复始。所以,看待操作系统,不仅是要从应用程序的角度去看,不仅是从X86的架构去看,作为运维开发工程师,更要从Linux内核调度调优的角度,去理解Linux操作系统本身。

所以,理解内核与其他的关系就是一条理解LInux操作系统本身的捷径。抽象内核与其他这层关系,在系统调优层面,就分为内核调优以及其他调优,其他调优包括了CPU性能、内存性能、磁盘IO性能以及网络性能的调优。



Linux操作系统的设计思想是一种主奴机制,操作系统的设计者依托于硬件,在主奴机制的思想指导下,把操作系统的内核与用户进程间的关系设计为“主子”和“奴才”的关系。其中,进程调度体现主奴机制,内存管理体现主奴机制,文件系统体现主奴机制。


1)、进程调度实现主奴机制

为了实现主奴机制,首先就要在操作系统内核程序与应用程序之间、应用程序与应用程序之间建立有效的边界。为此,现在操作系统的设计者提出了进程的概念,用task_struct数据结构来实现明确地划分边界的作用,而task_struct是进程最主要的标志。从操作系统的角度去看,进程就是运行中的接受操作系统组织、管理和协调的程序。

从技术角度看,创建进程的方式不止一种,Linux操作系统的进程创建采用的是对象创建模式。对象创建就是用已有的对象创建新的对象,用已有的进程创建新的进程,也就是所谓的父子进程创建机制。从本质上讲,创建进程的最主要就是创建task_struct。父子进程创建机制就是从父进程的task_struct复制一份作为子进程的tak_struct。从逻辑上反推,父子进程创建机制意味着最初的父进程必须独立存在,这就是进程0。不难理解,1进程0不能由父子进程创建机制创建,只能由操作系统设计者手工编写进程0的task_struct。有了进程0,父子进程创建机制就可以用进程0作为父进程创建子进程了。


2)、内存管理体现主奴机制

Linux内核和用户进程都是采用了分页机制,但是采用的管理数据却是两套:一套是针对内核使用,分页范围是0--16MB的全部内存空间;另外一套是针对用户进程,分页范围是1--16MB空间(1MB一下的都是内核空间)。

只要操作系统内核代码的内核专用内核专用区,进程是无论如何都不能访问到的,而内核的访问范围却囊括了整个内存空间。我独占的地方你来不了,你的地方我可以随便去,因为你的地方就是我的地方。这就是“普天之下,莫非王土”,结果必然是“率土之滨,莫非王城臣”。


3)、文件系统体现主奴机制

以写文件时申请磁盘空间为例子。当用户需要向磁盘写文件时,首先需要向内核空间提出申请,说明自己是哪个进程,自己需要的资源大小,以及资源的读写权限;内核接到申请以后,会结合当前的磁盘已占用资源、缓冲区实际情况是否立即满足该用户进程的请求。如果多个进程都在申请资源,内核会决定让某一个进程先获得资源,其他进程处于等待状态,并且对等待队列的排序做出管理。如果当前资源无法满足要求,内核还会驳回进程的申请,这也是充分体现了啮合和用户进程的主奴机制。








0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:8741次
    • 积分:193
    • 等级:
    • 排名:千里之外
    • 原创:14篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章分类