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学习系列,至少一周一更新,纯干货分享,无广告,不打赏。欢迎
转载
,欢迎
评论交流
!
往期链接: