os内核入门-linux0.11诞生的故事和源码初探

原创 thatway 那路谈OS与SoC嵌入式软件 2022-01-15 08:00

直接去解读源码可能比较枯燥,而且对阅读公众号的文章不友好,这里只是引导入门,拓展知识,阅读源码还是需要自己下一些苦功夫。所以后续文章尽量会用形象的语言,以源码为中心进行点到为止、深入浅出的描述。

1. Linux诞生的历史

演进:Multics->UNIX->MINX->Linux

下面要讲一个很长的故事,时间要倒退到20世纪60年代,这是一个大型、复杂操作系统盛行的年代,比如IBM 的
OS/360,这个是商用成功了的,但是Honey well(霍尼韦尔)的
Multics系统虽然持续存在了多年,却从来没有被广泛应用过,Multics项目的参与者之一
贝尔实验室因为该项目太复杂和进展缓慢而于1969年退出,Multics失败了但是其影响深远。

贝尔实验室退出了
Multics开发,但是这里牛人聚集,这里主要提两个人,一个是
Ken Thompson
,在1969年夏天他媳妇回娘家了,他闲着没事干,但是非常喜欢打游戏,为了在闲置的一个计算机上玩星际“星际旅行”游戏(之前在Multics上玩),一个月时间借鉴Multics开发出了
UNIX
的原型,是用B语言写的。第二个人DennisRitchie
,为了对UNIX进行改造发明了C语言,利用C语言对UNIX进行了重写。给了很多C程序员饭碗,开创高级语言书写程序的一个新时代。

UNIX出来后,贝尔实验室向学校提供源码,并发展了很多分支,例如BSD版本、System V版本、Solaris版本等,这个时代同样出现了微软的DOS系统和苹果的MAC系统,后面作为插曲
进行介绍。7080年代突然冒出来这么多OS,上层应用写的代码要在这些不同系统上适配,由于提供的API不一样,就很麻烦。IEEE(电气和电子工程师协会)就开始标准化这些接口,称作
POSIX
标准表示可移植操作系统接口(Portable Operating SystemInterface of UNIX,缩写为 POSIX )。

UNIX出名后,逐渐商业化,没有授权就看不到源码了。这个时候有个美国人Andrew S. Tanenbaum
在加州伯克利分校读的博士,上面说的BSD就是这个学校研究出来的,时间是重叠的。Andrew S. Tanenbaum虽然出生在美国但是祖上是荷兰人,后来他又回到荷兰Vrije大学教书了,这可是个大牛,为了给学生教OS原理的课,他在1987年写了一个
MINIX
操作系统,自称没参考UNIX的代码。他还写了书介绍这个MINX操作系统,完全公开。虽然只是教学的OS,但是一时大家找不到其他的可以免费阅读的源码资料,其他的OS源码都被软件商垄断保密起来了,并且商用OS价格非常的贵。

1991年,Linus Torvalds
在赫尔辛基大学计算机科学系上大二,这个同学从小喜欢计算机,爷爷是教授,从小就会计算机编程。他为了学习MINIX自己购买了Intel 80386的电脑,由于80386的一个硬件支持的新特性任务切换功能,使他编写自己的OS有了可能。另外GUN开源软件的兴起,带来了很多免费的工具,比如GUN C编译器、bash shell、gdb等。他想把GUN的这些软件移植到MINIX上,在这个过程中遇到很多问题,他去找同在荷兰的Andrew S.Tanenbaum请教,但是Andrew S. Tanenbaum不修改,而GUN组织也计划写一个OS,但是GUN一时半会还造不出来这个OS。所以迫使
L
inus Torvalds自己写一个OS来解决。经过快一年的折腾,1991年10月5日,
L
inus

Torvalds在comp.os.minix上对外宣布Free minix-like Kernel sources for 386-AT诞生,同样声称没有是使用MINIX一行代码。ftp服务器管理员
命名文件夹的时候使用了缩写
Linux
,就这样Linux正式诞生了。我们这里使用的0.11是1991年12月8日比较稳定可以正常运行的一个内核版本。Linux设计的接口符合POSIX,并且得益于Internt的发展,开源软件运动的兴起,随后Linux在世界各地广泛传播应用,最终到我们这个时代成为一方霸主。

上面我们了解一下linux0.11诞生的基础,这样有助于更好的去看待这份代码。下面配一张个性程序员的图,来开始闭源OS插曲的说明

插曲
:主流的windows和mac系统介绍:

windows DOS
系统:

1979年微软从AT&T获取授权并开发了运行于intel平台的Xenix(基于UNIX7)。1981年IBM的人问比尔盖茨
有可以在IBM PC机上运行的OS不,比尔盖茨推荐Digital Research公司的系统,但是Digital Research公司托大,不见IBM的人员。IBM回头又找到了比尔盖茨,比尔盖茨找到了一家本地西雅图电脑产品公司有一个DOS系统,一锤子价格5万美元买了过来,卖给IBM的时候变聪明了,是按照IBM卖出的机器,按每台进行提成,导致了后来大赚。DOS系统是一个蒂姆·帕特森(Tim Paterson
)的程序员花费了四个月时间编写,比尔盖茨又把这个人挖了过来,比尔盖茨商业的确很厉害。

MAC
系统:

要从图形界面说起,图形界面是美国的施乐在1981年推出的“施乐之星(Xerox Star)”电脑上携带的,但是卖的比较贵,买的人少。一天乔布斯
到施乐公司访问,见到了GUI界面的系统,马上意识到前景很好,于是1983年推出了图形界面操作系统Lisa OS(基于BSD),并配备了鼠标(也是跟施乐学的)。1983 年底,盖茨宣布微软即将推出 Windows 1.0,乔布斯得知后大发雷霆,差人将盖茨叫到了苹果总部,当着十来个研发团队成员的面,呵斥盖茨“你在盗用我们的东西!”接着,盖茨便平静地说出了后来成为硅谷经典反驳台词的那句话——“
我们都有个有钱的邻居,叫施乐,我闯进他们家准备偷电视机的时候,发现你已经把它盗走了
”。

上面的历史回顾以UNIX为核心充满了江湖纷争,但是不论什么都阻止不技术的发展。

Multics-》UNIX-》MINX-》Linux虽然都宣传没有使用对方一行代码,但是思想是相通的,有的是看不上别人的实现,而重写的。从哲学角度思考,
操作系统是人类设计的,所以操作系统的设计思想跟我们人类的思维办事方式是一致的
,越深入了解其机制会越有同感。

2. Linux源码初探

下面回归正题,从几个角度对代码有一个初步的接触,激发下对代码的兴趣。

2.1 如果你在电脑上看电影,电脑在做什么?

计算机有哪些硬件?

CPU、内存、硬盘、网卡、键盘、鼠标、USB、屏幕、主板、风扇等。

如果你在电脑上看电影,电脑在做什么?

fs->blk_drv->mm->cpu

首先电影XXX.rmvb在你电脑的硬盘上存储,当在播放器点击播放这个电影的时候,由于硬盘的速度比较慢不能把数据直接给CPU,所以内核的块驱动程序先将电影读入内存,播放器程序会把rmvb格式的数据计算成屏幕上显示的点,这就需要cpu,然后给到屏幕显示。另外播放器程序原本也是在硬盘上存储的。

代码目录如下:

linux-0.11/fs目录下放的文件系统相关代码,例如
open.c 文件的打开、关闭、创建
read_write.c  对文件的读写操作

linux-0.11/mm 目录下放的是内存管理程序
memory.c 建立内存中地址(程序使用也叫虚拟地址)和键盘中地址(物理地址)的映射。

linux-0.11/kernel/blk_drv  含有硬盘和软盘的驱动程序,负责对盘上的数据进行读写
ll_rw_blk.c 块设备的读写操作
hd.c 硬盘控制器程序

2 使用printf打印字符串到屏幕经历了什么?

printf->write->int $0x80->system_call->sys_write->rw_char->rw_ttyx->rw_tty->tty_write->con_write->寄存器操作

上一篇文章,我们在linux-0.11/init/main.c中添加了一行代码

printf("This my helloworld!\n\r");

printf函数在mian.c中定义,如下:

static int printf(const char *fmt, ...)
{
   va_listargs;
   inti;
   va_start(args,fmt);
   write(1,printbuf,i=vsprintf(printbuf, fmt, args));
   va_end(args);
   returni;
}

vsprintf(printbuf, fmt, args)是字符串的格式化,可以自己去研究下。

这里write是一个系统调用,下面看下这个wrire函数是怎么定义的

/lib/write.c中

_syscall3(int,write,int,fd,constchar *,buf,off_t,count)

include/unistd.h中

#define _syscall3(type,name,atype,a,btype,b,ctype,c) \                                         
type name(atypea,btype b,ctype c) \                                                           
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    :"=a" (__res) \                                                                                                              
    :"0" (__NR_##name),"b"((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \                    
if (__res>=0) \
   return (type) __res; \                                                                     
errno=-__res; \                                                                                
return -1; \                                                                                    
}

这里按照宏定义对write函数进行了定义翻译过来就是:

int write(int fd, const char *buf, off_t, count) {
    long __res;
    __asm {
        //... 参数传递
        mov eax, __NR_write //__NR_write的值定义为 4
        int 0x80            //这是重点,32位陷阱门
        //... 返回值处理
    }
    return __res;
 }

_NR##name的定义为

#define __NR_write  4

int $0x80系统调用会执行kernel/system_call.s

system_call:
all sys_call_table(,%eax,4)        # 间接调用指定功能C函数

sys_call_table在include/linux/sys.h中定义

fn_ptr sys_call_table[] = { sys_setup,sys_exit, sys_fork, sys_read,                                                              
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,。。。。。

由上面的_NR_write 4找到函数sys_write

fs/read_write.c中

int sys_write(unsigned int fd,char * buf,int count)
{
   if(fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))
          return-EINVAL;

   inode=file->f_inode;

   if(S_ISCHR(inode->i_mode))
              return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
   printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
   return-EINVAL;
}

fd的值是1,这里牵扯到tty设备的初始化,想了解了继续关注公众号讲到的时候说明

tty是一个字符设备,这里调用rw_char函数

在fs/char_dev.c中

if (!(call_addr=crw_table[MAJOR(dev)]))
       return -ENODEV;
   return call_addr(rw,MINOR(dev),buf,count,pos);

crw_table的定义为:

static crw_ptr crw_table[]={                                                                                                      
   NULL,       /* nodev */
   rw_memory,  /* /dev/mem etc */
   NULL,       /* /dev/fd */
   NULL,       /* /dev/hd */
   rw_ttyx,    /*/dev/ttyx */
   rw_tty,     /* /dev/tty */
   NULL,       /* /dev/lp */
   NULL};      /* unnamed pipes */

MAJOR(dev)了解驱动的同学都知道,有设备的主从设备号,通过命令查看如下

主设备号是4,这里对应rw_ttyx函数
,传入的minor为次设备号0

tty_write(minor,buf,count));

tty_write函数中下面三句话很重要:

tty = channel + tty_table
PUTCH(c,tty->write_q);
tty->write(tty);

tty = channel + tty_table

找到tty为0+ tty_table,就是tty_table结构体的第一个元素

在kernel/chr_drv/tty_io.c中

struct tty_struct tty_table[] = {
{
      {ICRNL,          /* change incoming CR to NL */
      OPOST|ONLCR,    /* change outgoing NL to CRNL */
      0,
      ISIG| ICANON | ECHO | ECHOCTL | ECHOKE,
      0,           /* console termio */
      INIT_C_CC},
      0,                  /* initial pgrp */
      0,                  /* initial stopped */
      con_write,
      {0,0,0,0,""},            /* console read-queue */
      {0,0,0,0,""},            /* console write-queue */
      {0,0,0,0,""}             /* console secondary queue */

PUTCH(c,tty->write_q);

把要写的数据放入队列write_q中

tty->write(tty);

tty->write 由上面的
tty_table找到对应这个con_write函数

 // 获取写队列的字符数量
nr =CHARS(tty->write_q);
while(nr--){
   // 每次获取一个字符c
   GETCH(tty->write_q,c);
  //写操作的核心代码:
  __asm__("movbattr,%%ah\n\t"
     "movw%%ax,%1\n\t"
     ::"a"(c),"m"(*(short*)pos)
     );
   这句嵌入汇编代码的意思是先把寄存器ax中存放字符c和其属性,其中字符在低位。然后将ax中的数据存放在地址为pos的内存处。这样就完成了一个字符的printf,后面的字符也是大同小异地写入到pos地址的内存处。具体汇编语言算是个拦路虎,后续文章也会进行介绍。  

上面就是printf的流程,是不是有点被这个分析吓到了,但是这个过程从软件角度是不是很深入,分析的很过瘾,软件干到寄存器
也算到头了
,不论啥时候回过来看这段分析,都会回味无穷。

后记:

文中的语言基本都是笔者参考了一堆的书和资料自己组织讲出来的,费了挺多时间。书写的过程也是对知识的二次整理,其实挺不容易,鼓励大家也多写点什么。这一篇业余时间写了一个星期,好像篇幅有点长,下次会短点。

linux0.11学习系列,至少一周一更新,纯干货分享,无广告,不打赏。欢迎  

转载
,欢迎
评论交流

往期链接:

os内核入门-linux0.11运行环境搭建

results matching ""

    No results matching ""