Linux内核源代码漫游
- 格式:doc
- 大小:94.00 KB
- 文档页数:9
[原创]linux2.6内核的编译步骤及模块的动态加载-内核源码学习-linux论坛05年本科毕业设计做的是Linux下驱动的剖析,当时就买了一本《Linux设备驱动程序(第二版)》,但是没有实现将最简单的helloworld程序编译成模块,加载到kernel里。
不过,现在自己确实打算做一款芯片的Linux的驱动,因此,又开始看了《Linux设备驱动程序》这本书,不过已经是第三版了。
第二版讲的是2.4的内核,第三版讲的是2.6的内核。
两个内核版本之间关于编译内核以及加载模块的方法都有所变化。
本文是基于2.6的内核,也建议各位可以先看一下《Linux内核设计与实现(第二版)》作为一个基础知识的铺垫。
当然,从实践角度来看,只要按着以下的步骤去做也应该可以实现成功编译内核及加载模块。
个人用的Linux版本为:Debian GNU/Linux,内核版本为:2.6.20-1-686.第一步,下载Linux内核的源代码,即构建LDD3(Linux Device Drivers 3rd)上面所说的内核树。
如过安装的Linux系统中已经自带了源代码的话,应该在/usr/src目录下。
如果该目录为空的话,则需要自己手动下载源代码。
下载代码的方法和链接很多,也可以在CU上通过/search/?key=&;q=kernel&a mp;frmid=53去下载。
不过,下载的内核版本最好和所运行的Linux系统的内核版本一致。
当然,也可以比Linux系统内核的版本低,但高的话应该不行(个人尚未实践)。
Debian下可以很方便的通过Debian源下载:首先查找一下可下载的内核源代码:# apt-cache search linux-source其中显示的有:linux-source-2.6.20,没有和我的内核版本完全匹配,不过也没关系,直接下载就可以了:# apt-get install linux-source-2.6.20下载完成后,安装在/usr/src下,文件名为:linux-source-2.6.20.tar.bz2,是一个压缩包,解压缩既可以得到整个内核的源代码:# tar jxvf linux-source-2.6.20.tar.bz2解压后生成一个新的目录/usr/src/linux--source-2.6.20,所有的源代码都在该目录下。
Linux操作系统内核源代码的表达方式Linux操作系统内核源代码的表达方式是指内核代码中所使用的语法和结构。
它决定了内核代码的可读性、可维护性和可移植性。
内核源代码的表达方式主要包括以下几个方面:语法:Linux内核源代码使用C语言编写。
C语言是一种广泛使用的通用编程语言,具有丰富的库和工具支持,并且很容易移植到不同的平台上。
结构:Linux内核源代码由许多相互关联的文件组成,这些文件包含了内核的各种功能和模块。
内核源代码的结构通常是分层的,每一层都包含了不同的功能。
命名约定:Linux内核源代码使用了严格的命名约定,这使得内核代码更容易阅读和维护。
例如,内核中的函数通常以“sys_”或“do_”开头,而结构体通常以“struct_”开头。
注释:Linux内核源代码中包含了大量的注释,这些注释解释了代码的功能和用法。
注释对于理解内核代码非常有用,尤其是对于新内核开发者来说。
Linux内核源代码的表达方式的特点Linux内核源代码的表达方式具有以下几个特点:简洁:Linux内核源代码非常简洁,代码中没有多余的注释和代码段。
这种简洁性使得内核代码更容易阅读和维护。
模块化:Linux内核源代码是模块化的,内核中的各个模块可以独立编译和加载。
这种模块化设计使得内核更容易扩展和维护。
可移植性:Linux内核源代码具有很强的可移植性,它可以移植到不同的硬件平台上。
这种可移植性使得Linux内核成为世界上最受欢迎的操作系统之一。
Linux内核源代码的表达方式的优点Linux内核源代码的表达方式具有以下几个优点:可读性:Linux内核源代码非常可读,代码中的注释和命名约定使得内核代码很容易理解。
这种可读性对于内核开发者来说非常重要,它可以帮助开发者快速理解和修改内核代码。
可维护性:Linux内核源代码也非常可维护,代码中的模块化设计使得内核更容易扩展和维护。
这种可维护性对于内核的长期发展非常重要,它可以确保内核能够不断更新和改进。
Linux内核源代码漫游Linux教程-编程开发-Linux内核源代码漫游本章试图以顺序的方式来解释Linux源代码,以帮助读者对源代码的体系结构以及很多相关的unix特性的实现有一个很好的理解。
目标是帮助对Linux 不甚了解的有经验的C程序员对整个Linux的设计有所了解。
这也就是为什么内核漫游的入点选择为内核本身的启始点:系统引导(启动)。
这份材料需要对C语言以及对Unix的概念和PC机的结构有很好的了解,然而本章中并没有出现任何的C代码,而是直接参考(指向)实际的代码的。
有关内核设计的最佳篇幅是在本手册的其它章节中,而本章仍趋向于是一个非正式的概述。
本章中所参阅的任何文件的路径名都是指主源代码目录树,通常是/usr/src/linux。
这里所给出的大多数信息都是取之于Linux发行版1.0的源代码。
虽然如此,有时也会提供对后期版本的参考。
这篇漫游中开头有图标的任何小节都是强调1.0版本后对内核的新的改动。
如果没有这样的小节存在,则表示直到版本1.0.9-1.1.76,没有作过改动。
有时候本章中会有象这样的小节,这是指向正确的代码以对刚讨论过的主题取得更多信息的指示符。
当然,这里是指源代码。
引导(启动)系统当PC的电源打开后,80x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。
PC机的BIOS将执行某些系统的检测,在物理地址0处开始初始化中断向量。
此后,它将可启动设备的第一个扇区读入内存地址0x7C00处,并跳转到这个地方。
启动设备通常是软驱或是硬盘。
这里的叙述是非常简单的,但这已经足够理解内核初始化的工作过程了。
Linux的最最前面部分是用8086汇编语言编写的(boot/bootsect.S),它将由BIOS读入到内存0x7C00处,当它被执行时就会把自己移到绝对地址0x90000处,并将启动设备(boot/setup.S)的下2kB字节的代码读入内存0x90200处,而内核的其它部分则被读入到地址0x10000处。
Bootsect.s(1-9)!! SYS_SIZE is the number of clicks (16 bytes) to be loaded.! 0x3000 is 0x30000 bytes = 196kB, more than enough for current! versions of linux ! SYS_SIZE 是要加载的节数(16 字节为1 节)。
0x3000 共为1 2 3 4 5 60x7c000x00000x900000x100000xA0000system 模块代码执行位置线路0x90200! 0x30000 字节=192 kB(上面Linus 估算错了),对于当前的版本空间已足够了。
!SYSSIZE = 0x3000 ! 指编译连接后system 模块的大小。
参见列表1.2 中第92 的说明。
! 这里给出了一个最大默认值。
!! bootsect.s (C) 1991 Linus Torvalds!! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves! iself out of the way to address 0x90000, and jumps there.!! It then loads 'setup' directly after itself (0x90200), and the system! at 0x10000, using BIOS interrupts.!! NOTE! currently system is at most 8*65536 bytes long. This should be no! problem, even in the future. I want to keep it simple. This 512 kB! kernel size should be enough, especially as this doesn't contain the! buffer cache as in minix!! The loader has been made as simple as possible, and continuos! read errors will result in a unbreakable loop. Reboot by hand. It! loads pretty fast by getting whole sectors at a time whenever possible.!! 以下是前面这些文字的翻译:! bootsect.s (C) 1991 Linus Torvalds 版权所有!! bootsect.s 被bios-启动子程序加载至0x7c00 (31k)处,并将自己! 移到了地址0x90000 (576k)处,并跳转至那里。
Linux内核(2.6.13.2)源代码分析苗彦超摘要:1系统启动1.1汇编代码head.S及以前设置CPU状态初值,创建进程0,建立进程堆栈:movq init_rsp(%rip), %rsp,init_rsp定义.globl init_rspinit_rsp:.quad init_thread_union+THREAD_SIZE-8即将虚地址init_thread_union+THREAD_SIZE-8作为当前进程(进程0)核心空间堆栈栈底,init_thread_union定义于文件arch/x86_64/kernel/init_task.c中:union thread_union init_thread_union __attribute__((__section__(".data.init_task"))) ={INIT_THREAD_INFO(init_task)};INIT_THREAD_INFO定义于文件include/asm-x86_64/thread_info.h中,初始化init_thread_union.task = &init_task,init_task同样定义于文件init_task.c中,初始化为:struct task_struct init_task = INIT_TASK(init_task);INIT_TASK宏在include/linux/init_task.h中定义。
全部利用编译时静态设置的初值,将进程0的控制结构设置完成,使进程0可以按普通核心进程访问。
init_task.mm = NULL; init_task.active_mm = INIT_MM(init_mm), init_m = “swapper”INIT_MM将init_mm.pgd初始化为swapper_pg_dir,即init_level4_pgt,定义与head.S中。
[文件系统]Linux文件系统—源代码导读众所周知,文件系统是Unix系统最基本的资源。
最初的Unix系统一般都只支持一种单一类型的文件系统,在这种情况下,文件系统的结构深入到整个系统内核中。
而现在的系统大多都在系统内核和文件系统之间提供一个标准的接口,这样不同文件结构之间的数据可以十分方便地交换。
Linux也在系统内核和文件系统之间提供了一种叫做VFS(virtual file system)的标准接口。
这样,文件系统的代码就分成了两部分:上层用于处理系统内核的各种表格和数据结构;而下层用来实现文件系统本身的函数,并通过VFS来调用。
这些函数主要包括:* 管理缓冲区(buffer. c)。
* 响应系统调用fcntl() 和ioctl()(fcntl.c and ioctl.c)。
* 将管道和文件输入/输出映射到索引节点和缓冲区(fifo.c, pipe.c)。
* 锁定和不锁定文件和记录(locks.c)。
* 映射名字到索引节点(namei.c, open.c)。
* 实现select( )函数(select . c)。
* 提供各种信息(stat.c)。
* 挂接和卸载文件系统(super.c)。
* 调用可执行代码和转存核心(exec.c)。
* 装入各种二进制格式(bin_fmt*.c)。
VFS接口则由一系列相对高级的操作组成,这些操作由和文件系统无关的代码调用,并且由不同的文件系统执行。
其中最主要的结构有inode_operations 和file_operations。
file_system_type是系统内核中指向真正文件系统的结构。
每挂接一次文件系统,都将使用file_system_type组成的数组。
file_system_type组成的数组嵌入到了fs/filesystems.c中。
相关文件系统的read_super函数负责填充super_block结构。
--------------------------------------------------------------------------------源码导读*Linux 如何维护它支持的文件系统中的文件*描述了虚拟文件系统(Virtual File System VFS )*解释了Linux 核心中真实的文件系统如何被支持Linux 的一个最重要的特点之一使它可以支持许多不同的文件系统。
Linux 内核解读入门针对好多Linux 爱好者对内核很有兴趣却无从下手,本文旨在介绍一种解读Linux内核源码的入门方法,而不是解说Linux复杂的内核机制。
1.核心源程序的文件组织(1)Linux核心源程序通常都安装在/usr/src/Linux下,而且它有一个非常简单的编号约定:任何偶数的核心(例如 2.0.30)都是一个稳定的发行的核心,而任何奇数的核心(例如2.1.42)都是一个开发中的核心。
本文基于稳定的2.2.5源代码,第二部分的实现平台为RedHat Linux 6.0。
(2)核心源程序的文件按树形结构进行组织,在源程序树的最上层你会看到这样一些目录:● Arch :arch子目录包括了所有和体系结构相关的核心代码。
它的每一个子目录都代表一种支持的体系结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。
PC机一般都基于此目录;● Include: include子目录包括编译核心所需要的大部分头文件。
与平台无关的头文件在include/linux子目录下,与intel cpu相关的头文件在include/asm-i386子目录下而include/scsi目录则是有关scsi设备的头文件目录;● Init:这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,这是研究核心如何工作的一个非常好的起点;● Mm :这个目录包括所有独立于cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等,而和体系结构相关的内存管理代码则位于arch/*/mm/,例如arch/i386/mm/Fault.c;● Kernel:主要的核心代码,此目录下的文件实现了大多数Linux系统的内核函数,其中最重要的文件当属sched.c,同样,和体系结构相关的代码在arch/*/kernel 中;● Drivers:放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录,如/block下为块设备驱动程序,比如ide(ide.c)。
linux内核源码arch⽬录的前世今⽣历史的痕迹:在最新的linux-2.6.31/arch/arm/⽂件夹下,仍然保留Linux最初向ARM处理器移植的痕迹,最初的移植由⿊客完成,在⽼的移植的代码⽂件的头部保留着⿊客的名字;最初的ARM也不像现在这样的先进,都是StrongARM、ARM7之类,具体⽂件夹如mach-sa1100、mach-shark、mach-rpc等。
现在就ARM来说已不是StrongARM、ARM7的时代,但勿忘历史,让我们记住那些把Linux移植到ARM的⼤⽜们,记住哪个开拓的年代。
夫妻的利益:当NOKIA的⼿机在世界流⾏时,和那句名⾔”成功的男⼈背后有⼀个⼥⼈,或者是成功的⼥⼈背后有⼀个男⼈“⼀样,NOKIA 的背后站着TI,可以认为NOKIA 2009年前的⼿机80%都是都使⽤了TI的OMAP处理器,换句话说OMAP处理器就是NOKIA和TI结婚证书,看⼀看mach-omap2下⽂件的开头有TI的地⽅⼤多就有NOKIA,也许你就能理解其中的深意。
但古话⼜说的好”夫妻本是同林鸟、⼤难来时各只飞“,在这个危机的年头,NOKIA和TI的关系也偶现危机,它们的未来还真说不好。
成功的失败:有部电影叫《真实的谎⾔》,套⽤⼀下就是“成功的失败”,INTEL是成功者,这样说肯定很少⼈回提出疑义,但成功者也有失败,INTEL在X86上风光⽆限,在XSCALE上却体味到少有的失败,从StrongARM到XSCALE,INTEL好象没有体味到多少成功,XSCALE 在⽹络处理器上的应⽤--IXP系列,到现在已经没有刚推出IXP4xx/IXP24xx时的意⽓风发,IXP4xx还有亮点, IXP24xx则完全是个残废;XSCALE在存储处理器--IOP系列上有点成功,多少能为INTEL换来些美⾦;最失败就是XSCALE在消费电⼦处理器上的应⽤-PXA系列,现在Marvell已经成为其主⼈。
Linux内核源代码漫游Alessandro Rubini著, rubini@pop.systemy.it赵炯译,gohigh@sh163net, ()本章试图以顺序的方式来解释Linux源代码,以帮助读者对源代码的体系结构以及很多相关的unix特性的实现有一个很好的理解。
目标是帮助对Linux不甚了解的有经验的C程序员对整个Linux的设计有所了解。
这也就是为什么内核漫游的入点选择为内核本身的启始点:系统引导(启动)。
这份材料需要对C语言以及对Unix的概念和PC机的结构有很好的了解,然而本章中并没有出现任何的C代码,而是直接参考(指向)实际的代码的。
有关内核设计的最佳篇幅是在本手册的其它章节中,而本章仍趋向于是一个非正式的概述。
本章中所参阅的任何文件的路径名都是指主源代码目录树,通常是/usr/src/linux。
这里所给出的大多数信息都是取之于Linux发行版1.0的源代码。
虽然如此,有时也会提供对后期版本的参考。
这篇漫游中开头有图标的任何小节都是强调1.0版本后对内核的新的改动。
如果没有这样的小节存在,则表示直到版本 1.0.9-1.1.76,没有作过改动。
有时候本章中会有象这样的小节,这是指向正确的代码以对刚讨论过的主题取得更多信息的指示符。
当然,这里是指源代码。
引导(启动)系统当PC的电源打开后,80x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。
PC机的BIOS将执行某些系统的检测,在物理地址0处开始初始化中断向量。
此后,它将可启动设备的第一个扇区读入内存地址0x7C00处,并跳转到这个地方。
启动设备通常是软驱或是硬盘。
这里的叙述是非常简单的,但这已经足够理解内核初始化的工作过程了。
Linux的最最前面部分是用8086汇编语言编写的(boot/bootsect.S),它将由BIOS读入到内存0x7C00处,当它被执行时就会把自己移到绝对地址0x90000处,并将启动设备(boot/setup.S)的下2kB字节的代码读入内存0x90200处,而内核的其它部分则被读入到地址0x10000处。
在系统加载期间将显示信息"Loading..."。
然后控制权将传递给boot/Setup.S中的代码,这是另一个实模式汇编语言程序。
启动部分识别主机的某些特性以及vga卡的类型。
如果需要,它会要求用户为控制台选择显示模式。
然后将整个系统从地址0x10000移至0x1000处,进入保护模式并跳转至系统的余下部分(在0x1000处)。
下一步是内核的解压缩。
0x1000处的代码来自于zBoot/head.S,它初始化寄存器并调用decompress_kernel(),它们依次是由zBoot/inflate.c、zBoot/unzip.c和zBoot/misc.c 组成。
被解压的数据存放到了地址0x10000处(1兆),这也是为什么Linux不能运行于少于2兆内存的主要原因。
[在1兆内存中解压内核的工作已经完成,见Memory Savers--ED]将内核封装在一个gzip文件中的工作是由zBoot目录中的Makefile以及工具完成的。
它们是值得一看的有趣的文件。
内核发行版1.1.75将boot和zBoot目录下移到了arch/i386/boot中了,这个改动意味着对不同的体系结构允许真正的内核建造,不过我将仍然只讲解有关i386的信息。
解压过的代码是从地址0x10100处开始执行的[这里我可能忘记了具体的物理地址了,因为我对相应的代码不是很熟],在那里,所有32比特的设置启动被完成: IDT、GDT以及LDT被加载,处理器和协处理器也已确认,分页工作也设置好了;最终调用start_kernel 子程序。
上述操作的源代码是在boot/head.S中的,这可能是整个内核中最有诀窍的代码了。
注意如果在前述任何一步中出了错,计算机就会死锁。
在操作系统还没有完全运转之前是处理不了出错的。
start_kernel()是位于init/main.c中的,并且没有任何返回结果。
从现在起的任何代码都是用C语言编制的,除了中断管理和系统调用的入/出代码(当然,还有大多数的宏都嵌入了汇编代码)。
让轮子转动起来在处理了所有错综复杂的问题之后,start_kernel()初始化了内核的所有部分,尤其是: ∙设置内存边界和调用paging_init();∙初始化中断、IRQ通道和调度;∙分析(解析)命令行;∙如果需要,就分配一个数据缓冲区(profiling buffer)以及其它一些小部分;∙校正延迟循环(计算“BogoMips”数);∙检查中断16是否能与协处理器工作。
最后,为了生成初始进程,内核准备好了移至move_to_user_mode(),它的代码也是在同一个源代码文件中的。
然后,所谓的空闲任务,进程号0就进入无限的空闲循环中运行。
接着初始进程(init process)尝试着运行/etc/init、/bin/init或者/sbin/init。
如果它们没有一个运行成功的,就会去执行代码“/bin/sh /etc/rc”并且在第一个终端上生成一个根命令解释程序(root shell)。
这段代码回溯至Linux 0.01,当时操作系统只有一个内核,并且没有登录进程。
在从一个标准的地方(让我们假定我们有)用exec()执行了init初始化程序之后,内核就对程序的执行没有了直接的控制。
从现在起它的规则是提供对系统调用的处理,以及为异步事件服务(比如硬件中断等)。
多任务的环境已经建立,从现在起是init程序通过fork()派生出的系统进程和登录进程来管理多用户的访问了。
由于内核是负责提供服务的,这个漫游文章将通过观察这些服务(“系统调用”)以及通过提供基本数据结构的原理和代码的组织结构继续讨论下去。
内核是如何看见一个进程的从内核的观点来看,一个进程只是进程表中的一个条目而已。
而进程表以及各个内存管理表和缓冲存储器则是系统中最为重要的数据结构。
进程表中的各个单项是task_struct结构,是定义在include/linux/sched.h中的非常大的数据结构。
在task_struct中保留着从低层到高层的信息,范围从某些硬件寄存器的拷贝到进程工作目录的inode信息。
进程表既是一个数组和双链表,也是一个树结构。
它的物理实现是一个静态的指针数组,它的长度是定义在include/linux/tasks.h中的常量NR_TASKS,并且每个结构都位于一个保留内存页中。
这个列表结构是通过指针next_task和pre_task构成的,而树结构则是非常复杂的并且我们在此将不加以讨论。
你可能希望改动NR_TASKS的默认值128,但你要保证所有源文件中相关的适当文件都要被重新编译过。
在启动引导过程结束后,内核将总是代表某个进程而工作,并且全局变量current --- 一个指向某个task_struct条目的指针 --- 被用于记录正在运行的进程。
current仅能通过在kernel/sched.c中的调度程序来改变。
然而,由于所有的进程都必须访问它,所以使用了宏for_each_task。
当系统负荷很轻时,它要比数组的顺序扫描快得多。
进程总是运行于“用户模式”或“内核模式”。
用户程序的主体是运行于用户模式而其中的系统调用则运行于内核模式中。
在这两种执行模式中进程所用的堆栈是不一样的 -- 常规的堆栈段用于用户模式,而一个固定大小的堆栈(一页,由该进程所有)则用于内核模式。
内核堆栈页是从不交换出去的,因为每当一个系统调用进入时它就必须存在着。
内核中的系统调用(system calls)是作为C语言函数存在的,它们的‘正规’名称是以‘sys_’开头的。
例如一个名为burnout的系统调用将调用内核函数sys_burnout()。
系统调用机制在本手册的第三章中进行了讨论。
观看在include/linux/sched.h中的for_each_task和SET_LINKS能够帮助理解进程表中的列表和树结构。
创建和结束进程unix系统是通过fork()系统调用创建一个进程的,而进程的终止是通过exit()或收到一个信号来完成的。
它们的Linux实现位于kernel/fork.c和kernel/exit.c中。
派生出一个进程是很容易的,所以fork.c程序很短并易于理解。
它的主要任务是为新的进程填写数据结构。
除了填写各个字段以外,相关的步骤有:●取得一个空闲内存页面来保存task_struct●找到一个空闲的进程槽(find_empty_process())●为内存堆栈页kernel_stack_page取得另一个空闲的内存页面●将父辈的LDT拷贝到子进程●复制父进程的mmap信息sys_fork() 同样也管理文件描述符和inode。
1.0的内核也对线程提供某些不够完善的支持,所以fork()系统调用对此也给出了某些示意。
内核的线程是主流内核以外的过程产品。
从一个进程中退出是比较有窍门的,因为父进程必须被通告有关任何子进程的退出。
而且,一个进程可以由另外一个进程使用kill()而退出(这些是Unix的特性),所以除了sys_exit()之外,sys_kill()以及sys_wait()的各种特性也存在于exit.c之中了。
这里不对exit.c的代码加以讨论---因为它一点也不令人感兴趣。
为了以一致的状态退出系统,它涉及到许多细节。
而POSIX标准对于信号则是要求相当严格的,所以这里必须对其加以叙述。
执行程序在调用了fork()之后,就有同一个程序的两个拷贝在运行了,通常一个程序使用exec()执行另一个程序。
exec()系统调用必须定位该执行文件的二进制映像,加载并执行它。
词语‘加载’并不一定意味着“将二进制映像拷贝进内存”,因为Linux支持按需加载。
exec()的Linux实现支持不同的二进制格式。
这是通过linux_binfmt结构来达到的,其中内嵌了两个指向函数的指针--一个是用于加载可执行文件的,另一个用于加载库函数,每种二进制格式都实现有这两个函数。
共享库的加载是在exec()同一个源程序中实现的,但我们只讨论exec()本身。
Unix系统提供了六种exec()函数。
除了一个以外,所有都是以库函数的形式实现的,并且,Linux内核是单独实现sys_execve()调用的。
它执行一个非常简单的任务:加载可执行文件的头部,并试着去执行它。
如果头两个字节是“#!”,那么就会解析该可执行文件的第一行并调用一个解释器来执行它,否则的话,就会顺序地试用各个注册过的二进制格式。
Linux本身的格式是由fs/exec.c直接支持的,并且相关的函数是load_aout_binary和load_aout_library。