首发于个人博客网站(prajna.top) 欢迎你们前去交流,有pdf版本。node
本文主要是从应用的角度出发,分别阐述操做系统接口,计算机语言,文件系统等背后的一些知识,规范,原理,设计思想,应用法门,让初学者对编码有一个总体的,全局的认识,有一个物理的视角,找到本身的起点。python
写这篇文章主要是基于本身大学的经历,当时抱着一腔热血去学计算机编程,但是当把c/c++语言,数据结构,操做系统,计算机组成原理等课程都学完后,却发现本身彷佛什么也不会,只会printf打印一些字符串。那段时间真的好苦恼,特别想作软件,殊不知从何开始,也不知道该如何去使力,蹉跎了很久,浪费了大量的时间。linux
形成这种现象的主要缘由,一是本身缺乏那种天赋,二是教学过于侧重基础和理论,每门课程只涉及到一个局部,没有一门课程把这些串起来。我不了解语言的基础库,除了printf后,其它API都不会用;也不了解具体的操做系统平台的API;虽然学了TCP/IP,socket的具体使用却又不清楚; 至于像fat,ext等磁盘文件系统的格式,那就更遥远了;我甚至还不清楚计算机语言和编译工具的关系;更要命的是,我还不知道本身不知道这些。说白了就是理论同应用脱节,虽然大学也安排了课程设计,实验和实习,但都只是走了过场,也没有人来指点一下,该看些什么书籍。大学读完,就知道拖拉几个控件作一个窗口,链接一下数据库。c++
windows + VC屏蔽了太多的技术细节,惋惜大学期间接触的恰恰就是它,对用户这个是好事越傻瓜越好,但是对计算机的学生就要了命了。自从我转投到linux开源世界后,终于才发现了什么是自由,什么是编程。当阅读linux源码碰到不理解的地方,能够直接修改源码加上打印,分析kernel流程。对比着minix的源码来学习操做系统结构原理,那些概念就变成实实在在的数据结构和算法,动手写一个minix的驱动,微内核和宏内核区别就一目了然了。强烈建议,想学习编程的同窗都去拥抱开源世界,而后,再回到本身感兴趣的或工做相关的领域。程序员
计算机语言说白了就是工具,关键仍是你要作什么,这样就涉及到了应用,以及专业背景知识。如想作驱动编程,离不开对操做系统驱动架构的了解;想作一个磁盘分区合并,那须要了解文件系统的格式;作个播放器吧,那对视频文件格式,编码格式,编解码API的了解必不可少。 随着你对软件系统了解的深刻,会发现其实一切都是协议。 http 是一套web 通信的协议;计算机语言是开发工具提供的协议; 操做系统是内核空间与应用空间的协议..., 这些协议被各类规范约束--并造成了各类技术。 因此,每种技术的背后都一套协议,规则和思想。了解这些才能算真正了解了相关技术。web
在这篇文章里面,我以GNU/Linux做为平台,从应用的角度出发来把相关的课程来串一串提供一个“物理视图”,让初学者有个全局的认识,可以有一个方向和切入角度,至少知道该找些什么资料来看。算法
它是内核对应用空间提供的一套协议,主要包括:数据库
ELF是编译, 连接生成的,执行的时候,由ld 解析,加载在到内存,最后控制权交给程序入口代码,程序开始执行。所以,它提供了2类视图:连接视图和执行视图。 编程
从连接视图上看,ELF由众多的 Section组成,编译器先把源码编译成.o文件,主要是提取函数,全局变量等生成符号表,把它们填充到相应的 Section里面去。 在这个阶段,全部的符号都是无法定位到地址的。windows
Link的时候,对.o文件进行合并,对各个文件内的符号进行重定位,安排它们的地址,以下图所示, link完成后,g_u8 和 g_flag2都有地址了。
对于动态连接的函数,在link阶段无法安排地址,须要放到 dynsym Section里面去,在 ld的时候,来进行定位 -- 这就是所谓的 "函数重定位"。
linux系统提供了可执行程序readelf来解析 ELF文件格式,咱们可使用它来了解一下ELF文件的一些通用的Section。
'offset Align' 是各个Section在ELF文件内的偏移地址,咱们以二进制的方式打开ELF文件,根据偏移地址,就能够查看相应Section的二进制内容。
从下图中能够看到 .interp的内容是 "/lib64/ld-linux-x86-64.so.2",
上面这些就是编译,连接生成ELF文件的过程:编译器以源文件做为输入,先提取各个文件的全局变量和函数,生成符号表,再把它们连接到一块儿,连接的时候对各个符号进行定位,分配地址。对于动态连接库的函数,则推迟到'加载'程序到内存的时候进行定位。编译连接后,代码和数据分散到了相应的section里面,程序加载的时候,须要把Section 合并成Secgment,而后,以Secgement为单位加载到内存页面里面去,咱们来看一下Segment的结构。
ELF有9种Segment,其中比较核心的是 --
Segment同Section 是有对应关系的,如:
程序执行的时候,先从INTERP 段找到对应的执行程序--可执行程序通常是ld.so, 首先加载ELF文件,根据Prgrame Header 数据结构,把section加载到各个程序段里面, 而后,递归重定位动态连接符号,加载这些符合依赖的动态连接库,处理ELF文件中的重定位, 而后,把控制权交给代码入口,程序开始运行。在这个过程当中,ld 最重要的一个事情就是'重定位', 修改ELF里面的动态连接函数的符号表。
系统调用是kernel提供给应用层的API,经过软中断来调用。调用的形式是这样
对ENTER_KERNEL宏的定义,传统(i386)的调用方式是 'int $80', ia64是 'syscall'指令来进入kernel。 软中断的流程基本上都是这么几个步骤
咱们能够在'arch/x86/syscalls/'目录下找到 syscall的列表,
最前面的数字就是系统调用号, 如: 'syscall 5'调用的是'open'。 总共大约是400个左右,涵盖了最基本的应用:如上图中文件相关的操做, 进程类的(fork, execve)等。 这些系统调用都被libc库作了封装, 一些简单的底层函数(如:mount, mkdir, stat)则只是简单地被包裹了一下,直接软中断到kernel了。因此,你们若是想了解文件系统相关API的实现,必定要看kernel的源码,看 libc库的源码是没用的,它都是简单地作了一个系统调用的转换。
前面提到过一切都是协议,POSIX是一个普遍被支持的协议(规范),Linux和各种unix都对它提供了支持,只要操做系统申明支持POSIX接口,它就得实现POSIX定义的系统调用。对linux/unix而言, POSIX只是它们的一个子集,它会还会支持UNIX世界的一些系统调用规范。总之,有了这些规范,libc就能够在各个系统间无缝移植。
sysfs是linux kernel以文件系统的方式提供给应用层的接口,在linux的世界里,驱动模块都被抽象为文件系统节点,所以,咱们对 /sys/文件系统进行读写操做,能够与内核里面的驱动层进行交流。 具体的接口内容,请查询 '/Documentation/ABI/'目录下sysfs-module文件
计算机语言就是一套人机交互的协议,比如咱们学了英语,就能够同“支持”英语的人交流,来达到咱们的一些目的。计算机语言的本质也是同样的,程序员经过某语言来调配它的资源,完成目标任务。不管是什么语言,语法方面都大同小异,无非就是变量定义,表达式加几个循环而已--它的理论源头就是大名鼎鼎的图灵完备的编程语言。图灵从理论上论证了,只要符合图灵完备规则,就能够知足全部的自动化计算的须要。
所以,各类语言的语法都大同小异,那么,为何咱们还须要那么多的语言呢?
这就回到了“什么是计算机语言的本质“这个问题了,语言的核心究竟是什么呢? 很显然不是语法,而是它编程思想和资源(功能)。 每种语言都是为了解决某些问题而存在,都会提供一套语法和平台资源--包括标准库和第三方库。
汇编语言主要是为了方便记忆,对机器指令上作了一个替换,没有汇编语言,咱们还须要一边查着芯片手册,一边手敲着 "6F 01 20" 之类的代码。因为它没有提供内存管理和系统结构化的手段,本质上仍是机器指令级别的编程。使用汇编语言,咱们还得规划内存,挑选寄存器,完成堆栈操做,所以,汇编只适合代码量少的系统。如很小的单片机或者系统的引导代码。高级语言则不一样,编译器帮忙搞掂了内存布局,进栈出栈等这些烦琐的事情,直接面向应用。
C语言提供了面向模块的结构化思想,经过模块化机制,咱们能够分工合做,构建起应用。学习C语言,就是要了解它的模块化思想,学会如何利用数据结构和函数指针封装模块,提供统一对外接口。另外,还得了解它的库,这个决定了咱们能得到多大的资源支持。C的基本库对系统调用进行了封装, 提供了一些跟操做系统相关的函数(文件操做,进程管理,内存相关,socket等),它没有提供经常使用的数据结构和算法(好比:链表的构建和排序,二叉树),须要本身处理。 在应用层面,它则提供了字符和字符串处理,数学函数(sin,cos,tan,...),日期和时间等等,可见C语言的标准库提供的都是底层的函数,固然了也有一些上层的GUI框架利用C语言提供接口的,他们在C的基础上提供大量的扩展,一步步地架构了本身的系统。
C++虽然是C的扩展,但它是全新的语言,提供的是面向对象的架构思想。只不是兼容C语言规范,它能够利用C语言的全部资源。它利用模板来提供标准库(STL)封装了经常使用的数据结构和算法。 官方又提供的 Boost模板库,拥有了更丰富更强大的功能。 利用STL/Boost和基础C库,都足以开发一些底层软件。 C和C++的基础库都没有提供图形,多媒体,GUI框架,用户须要使用第三方的资源,如:SDL, opengl,Qt等。
像JAVA这类带虚拟机的语言最厉害的地方,除了跨平台性,就是它强大的类库。不但封装了经常使用的数据结构和算法,还集成了GUI框架,图形库,多媒体处理,也有很强的web处理能力。
python则提出不要重复造"轮子",由于它提供了大量可用的轮子,从基础的数据结构到大型的应用模块,应有尽有。几句代码就能够完成一个 http服务器。 并且,python把变量定义这个环节都省了,直接面对要解决的问题,效率极高,特别适合教学,科研,作算法分析和原型验证,工具软件。 你想一想,如今你忽然有了一个算法构想,立刻想验证一下,使用的python的话,你直接就能够写算法代码了,想当于把算法的伪码拿过来直接就用了。
用C语言的话,你得先定义各类数据结构,变量,再编译,排错,真的是很会很急人的,若是涉及到字符串的处理,心早就拔凉拔凉了。
软件编程就是利用语言来调配各类资源来实现目标。学习一门新语言,除了学习语法,更多的关注点仍是它提供的编程思想和它的平台资源。你得了解目标语言提供的各类资源,了解它的适用场景,它是为解决什么问题而存在。最好的学习方法是看一些经典的源码,如:mplayer的C语言代码,很好地诠释了什么是模块化设计,真的使人叹为观止,会发现原来本身根本就不会写代码。
学汇编的比C语言厉害些吗?
常常会听到这样的争论,有一次地铁上,我在用C++编码,旁边一个”哥们“问我学习C++是否是赚钱些,我唯苦笑不已。语言自己没有高下之分,会用这个语言来完成任务才算厉害,最厉害的就是作出产品,像 freebsd, linux 这种划时代的产品,才是真的厉害。对程序员而已,厉害的是你的编程思想和算法能力,行业的专业知识。语言这种东西,只是是信手拈来,实现你的想法而已。
编译器就是语言的某一个具体实现,厂家不一样,产品不一样,像经典的 TC 和 VC。 编译器最基础的功能就是把所支持的源语言,编译成操做系统支持的可执行文件格式。若是,它再提供一个GUI界面,增长了源码编辑,断点调试,GUI框架,那么,它就成了一个开发平台,好比经典的VC。 目前,咱们使用的集成开发环境都是融合了编辑,编译,项目管理,资源编辑,GUI控件布局,代码生成等一系列工具。
对于初学者仍是建议你们本身使用GCC来做编译器, GDB来调试, 本身写Makefile来定义编译规则,再找一个源码编辑工具像Emacs或者Eclipse之类,这样你们能够很清晰地知道本身在干什么,须要作什么。再使用集成开发工具的时候,知道该怎么去处理各类问题,否则一直稀里糊涂地不不知道一个'Run'点下去,到底发生了什么,一出问题就傻眼。
若是须要重量级GUI框架的,能够考虑Qt平台,初学者也不要使用Qt Creator,能够本身使用Qt的工具来作预处理,如:
能够定义相应的规则,把它放到Makefile里面,这样就可使用make来进行管理。
话说学好了数据结构和算法,就基本解决了编程问题。但是当咱们拖拉几个控件,写几个事件,就能够完成工做的时候,不由会有点疑惑,说好的数据结构和算法呢。--其实,它无处不在,在应用框架里,在中间件里,在API里面,在 Kernel里。 C++的STL和JAVA都把一些经常使用的数据结构封装成了“集合”对象。
数据结构就是对客观对象的抽象,算法则是如何来组织和调用这些数据。在kernel里面,咱们耳熟能详的概念都是一个个具体的数据结构如:进程,内存页面等等。下面是linux的进程数据结构'struct task_struct'部分片段,能够看到对进程的描述信息(属性)都定义在该结构里面了。
这个就是咱们经过ps命令看到的进程信息描述,其实在linux kernel都是按照线程来管理的,因此,task_struct 也是线程的描述。 linux 源码里面就是大量的这种数据结构定义,以及把它们连接起来进行管理的各种链表,二分查找树。
下面简单说下数据结构和算法在STL里面的应用。
STL做为C++的标准库,它封装了经常使用的数据结构和算法。<vector> 的数据结构本质上是动态数组,当空间不够的时候,它从新分配空间,并拷贝旧元素到的数组。
<list> 是一个链表,“读”和“ 插入”的时间复杂度是O(n)--n是元素的位置, “插入”数据操做的时间消耗是常量级的。
虽然<vector>的'插入'时间复杂度也是O(n),可是,<list>不须要拷贝后面的元素,只须要移动到相应的位置,它的'插入'性能更好,而<vector> 提供随机访问能力,适合经过'数组下标'直接访问场合。
有没有像<vector>那样提供随机访问能力,可是又能提供 <list> 那样的插入性能的数据结构呢?
有,那就是 <deque>,它至关因而 <vector> 和 <list> 的一个结合,至关于 <list> 来链接固定大小的<vector>。
那是否是可使用<deque>来代替<vector>和<list>呢? 固然不能够:
<set>和 <map> 一般用一棵红黑二叉树来作为数据结构的实现,根据关键字来排序,“查找”,“插入”和”“删除”的时间开销都是 O(lg(n)),它里面的记录是有序排列的,咱们根据关键字把它导出来就是一个有序列表,它们的综合性能比较好。
若是须要更快速的访问能力,能够考虑使用带hash结构的 <unordered_set> 和 <unordered_map> “查找”,“插入”和”“删除”的平均开销是“常量级”的,性能很好,可是它里面的数据是“无序”的。
文件系统就是用来对文件进行增删改查的控制系统,不一样类型的文件系统对应着不一样的管理策略,可是,不管何种策略都须要解决两个最基本的问题:
所以文件系统须要在磁盘记录一些额外的信息,因此格式化之后,磁盘容量会小于实际的容量。格式化的过程就是在磁盘上“安装”文件系统的过程,下面以EXT2和FAT32为例来讲说文件系统是如何工做的。
ext系列是linux下面主打的文件系统,它以Block为容量单位,以Inode来抽象文件(目录也是文件)。一个硬盘分红多个Block Group,每一个Group里面分别存储Inode和具体的文件数据,以下图所示:
从上面图中能够看到,每一个Group Block的结构都是如出一辙,除了Data Blocks外,内容也都是同样的吗?
由于Super Block和 GDT是很是重要的,因此,每一个Block Group都有一份备份,所以他们的数据是同样的,因为备份会浪费空间,新版本的EXT系统再也不要求每一个Group都进行备份了,格式化的时候,能够选择备份策略。除了这两项外,其它的内容就跟该Group的存储的数据有关了。
那么划分Block Group的好处有哪些呢?
主要是为了防止文件存储碎片化。在存储的时候,预先保留多余空间,尽可能把文件放到一个Group里面,所以文件不是互相连续存放的:好比前后建立2个文件,第一个文件放在213 Block,第二个文件可能就510 Block,预留一部分空间给文件扩展用。固然了,对超大的文件,是会跨Group存放的。 经过精心的Block Group划分,再加上Linux灵活的文件分配策略,EXT文件系统在正常使用状况下“碎片率”是比较低的。
目录表没有放到Inode Table里面,而是放到Data Blocks上了,每一个目录节点都对应一个Inode,它的数据区域(Data Blocks)就是它的目录表。每一个目录表项包括:文件名,类型和Inode号--经过Inode号又能找到下一级目录表,这样就构成了一个目录链表结构。
类Unix系统都有一个“根目录”,是路径的起始点,系统就是从根目录开始来遍历目录链表的,它也至关于链表的“根节点”,所以它的位置必需是固定的:Linux系统的“根目录”Inode节点号是2。
下面以一个小容量的ext2系统为例来遍历下目录。
首先查找“根节点”,它的内容以下:
BLOCKS: (0):204 表示:该Inode只占用了一个Block(编号从0开始起),内容在204号Block上,咱们如今读取Inode 2 里面的内容(也就是204号Block):
根据目录表项的定义解析二进制数据,就能够获得根目录的目录表:
Name | Inode | Type |
---|---|---|
. | 0x02 | 目录 |
lost+found | 0x0b | 目录 |
home | 0x501 | 目录 |
large.img | 0x0c | 文件 |
接下来,遍历下一级目录。
若是咱们要访问"/home"目录,经过查找表,会发现它的Inode号是1281, 而后,读取Inode 1281的内容,再根据BLOCKS项的信息,读取它的数据块,又能够获得'/home'的目录表--就如同前面读取“根节点”同样,只是Inode 2变成了Inode 1281。这样反复递归,就能够一级一级地查询到最终的目录或者文件。
以上就是目录存储和查找的过程,目录表存储在Inode的数据区域,查找就是从根节点开始,反复递归,直到目的路径。
为何“根目录”的Inode号是2,而不是1或者0呢?
Inode 0 表示该Inode 不存在,相似C语言的空指针,这个是为了编码方便。 Inode 1是用存储坏块的, 因此,“根目录”就是Inode 2了,固然了,不一样操做系统的具体实现估计会有差别。
咱们经过目录结构找到文件对应的Inode节点后,就能够读取它的内容了。Inode节点使用一个数组来存储文件所占用的Block号(BLOCKS项的内容), 数组的长度为15,Block号用4个字节来表示。数组的前12位是当即寻址,第13,14和15位则是间接寻址。
下面以Block大小为1024个字节为例,来讲明这个寻址过程。
如上图所示:
分析一下:文件最多可占用(12+256+256256+256256*256 =16843020)个Block,若是Block的大小是1K,那么文件大小的上限约是 16G。对于小于13个Block的文件是直接寻址,访问速度快。间接寻址,主要是为了增大文件的容量,同时也能快速读取前12个Block的内容。
FAT是Windows下的一款经典文件系统,当时Windows系统下必定要作两件事情是:碎片整理和杀毒,而这些都同FAT文件系统有关。下面先了解下FAT系统的基本内容,下图是FAT的“物理视图”。
BPB (启动引导区域)-- 至关于FAT文件系统的“头部”,定以了FAT文件系统的“元数据”:扇区大小,簇的大小,FAT表的位置和大小,根目录的位置,基本上FAT文件系统的物理布局,都定义在这个里面,在系统格式化的时候生成。
FAT--全称是File Allocation Table(文件分配表),实质上就是一个大数组,以“簇”为单位 ,来记录硬盘空间的使用状况:
目录结构--目录也被看成(抽象)成一种文件,它的内容就是一个目录表。目录项的定义以下:
根据上面信息,能够画出FAT16物理布局图,
下面咱们以一个只有10M的FAT16文件系统为例,来实际说明一下FAT文件系统的工做过程。首先,解析BPB区域, 获得FAT16的“元数据”,部分重要信息以下:
根据上面信息,能够画出FAT16物理布局图,
由于根目录是特殊的数据,也能够说数据区是从0x9200开始的。
接下来找到根目录,读取它的目录表:
根据目录项的定义,获得根目录内容以下:
文件地址的物理簇号是从2开始算起的,所以,ROOT_F~1.IMG的起始物理地址(以字节为单位)计算以下:
文件起始地址: 0x9200 + (3 - 2) * 2048 = 0x9A00。
0x9A00就是文件的起始地址,当咱们读完该簇后,该如何去寻找下一个簇呢?
由于FAT16是的FAT表项是2个字节,所以 FAT[3]是0x04, 也就是说下一个文件簇的位置是4, 完整的查找过程以下:
能够看出 FAT就是一个连接,当前的值指向下一个簇号,直到以0xFFFF做为结束。
同理读取目录也是同样的,本例中,HOME目录的起始地址是: 0x9200 + (0x0d - 2) * 2048 = 0xEA00
0xea00的内容同根目录同样,就是该目录下的目录表,若是该目录的内容超过一个簇,也一样去经过FAT表去查找其它的内容。
这就是FAT文件系统的基本概念和工做流程。不难看出FAT采用的这种链表结构,会致使碎片化会很严重,使用时间长了之后,一个文件的簇链获得处都是。另外,FAT尚未权限管理,病毒程序就如入无人之境,能够随意复制和破坏。FAT的好处是简单灵活,文件的个数不固定,且占用磁盘空间少。可是,毕竟适应不了目前大容量高性能的要求,微软从FAT12打补丁到FAT32后,就推出了NTFS文件系统。
到这里,文件系统的一些基础知识就介绍完了,那么了解它有哪些实际的意义呢?
首先,像电影里面的黑客,能够直接面对文件系统的原始数据,好比:从文件系统损坏的硬盘里面恢复一些关键文件--经过读取磁盘文件系统的元数据,好比说直接找目录表,看看哪些文件还能够被识别,再查找相应的Inode节点(或FAT表),读取它的BLOCKS(或簇),只要物理上还没损坏,就能恢复出来。
其次,能够作一些磁盘类的工具,像文件搜索,恢复删除文件之类--文件被删除后,文件系统通常都是修改了些标志位,如:bitmap的空闲标志置1,表示空间被释放等,其实文件的内容还在硬盘上,只要及时地找到文件的位置,就有恢复的可能。我曾经用python作过一个小工具,能够浏览,提取iso 9600文件系统(光盘)里的文件,这样能够在不使用虚拟光驱的状况下,把iso镜像文件的内容都提取出来。
欢迎你们来个人网站交流:般若程序蝉