560 likes | 989 Views
Linux 内核分析入门. 西安交通大学 李思 2004 年 8 月 26 日. 主要内容. 有关 Linux 内核的基础知识 实模式与保护模式 用户空间与内核空间 Linux 内核源代码导读 工具与策略 内核源码目录结构 实例分析 Linux 启动代码分析 Linux 进程调度代码分析 Linux 设备驱动程序设计概述. 1. Linux 内核的发展. 内核源代码的行数. 2. 有关 Linux 内核的基础知识. (左移四位). 16 位段地址. 软件地址:. 16 位段地址. :. 16 位段内偏移. 物理地址:. 16 位段内偏移.
E N D
Linux内核分析入门 西安交通大学 李思 2004年8月26日
主要内容 • 有关Linux内核的基础知识 • 实模式与保护模式 • 用户空间与内核空间 • Linux内核源代码导读 • 工具与策略 • 内核源码目录结构 • 实例分析 • Linux启动代码分析 • Linux进程调度代码分析 • Linux设备驱动程序设计概述
(左移四位) 16位段地址 软件地址: 16位段地址 : 16位段内偏移 物理地址: 16位段内偏移 + = 20位物理地址 实模式 • BIOS 、Dos都工作在实模式下 • 实模式的缺点: • 寻址空间只有1M • 不能实现虚拟存储器,不能实现进程的隔离
保护模式 • Linux初始化完成后工作在保护模式 • 保护模式的优点: • 寻址空间可达4G,虚存,进程相互隔离 • 寻址方式: • 以32位段寄存器的值(段号+)作为索引查描述符表(段表),找到对应的描述符(基址、长度等) • 检查32位的偏移量是否超过段长度 • 检查权限是否正确 • 物理地址 = 基址 + 偏移量
两种模式的比较 实模式 保护模式
用户空间与内核空间 • 用户空间:一般应用程序所访问到的范围。它的特权级别低,不能直接访问底层的资源,且每个进程的用户地址空间是相互独立的。用户空间的进程是可以被内核进程所中断的。 • 内核空间:内核与内核模块所访问到的范围。它的特权级别最高,可以直接访问底层资源。内核地址空间只有1个。一般情况下,内核进程不能被任何用户进程所打断。 • 常用的用户空间与内核空间通信方式 • 通过设备文件 • 使用/proc文件系统 • 使用netlink套接字通信 • 通过系统调用传递数据
LKM • LKM = Loadable Kernel Module, 可加载的内核模块。当不发生歧义时,简称为内核模块,或者模块。 • LKM工作在内核空间下,一般看作是内核的一部分 • LKM是一种可加载/卸载的内核功能块 • LKM可以直接使用内核中的全局变量、调用内核中导出的其它函数 • LKM与静态编译到内核中的功能模块,在地位上是完全平等的 • 驱动程序一般使用LKM来实现 • 一般开发者的原则: 尽量多写模块,少改内核
读核工具 • Source Insight • 只有Windows版 • 在Linux下可以用wine来运行 • Source Navigator • 类似于前者,但有Linux版本 • LXR引擎:Linux Cross Reference • 根据内核源代码创建HTML页面 • 可以随意搜索标号的定义及其引用 • http://lxr.linux.no
读核策略 • 版本选择:可先读低版本,再读高版本 • 纵向与横向 • 纵向:按执行顺序阅读 • 横向:按功能模块来阅读 • 精读与泛读 • 全局泛读,局部精读 • 对于一些一时无法理解的部分,如果不是研究的重点,则要果断跳过,但切不可贪多求快! • 参考资料的合理使用 • 应该积极利用各种参考资料 • 不要过分迷信参考资料 • 要注意内核版本的区别!
Linux源代码目录结构 /usr/src/linux scripts Documentation ipc kernel init net arch mm lib fs drivers include 802 appletalk atm ax25 bridge core decnet econet ethernet ipv4 ipv6 ipx irda khttpd lapb … acorn atm block cdrom char dio fc4 i2c i2o ide ieee1394 isdn macintosh misc net … adfs affs autofs autofs4 bfs code cramfs devfs devpts efs ext2 fat hfs hpfs … asm-alpha asm-arm asm-generic asm-i386 asm-ia64 asm-m68k asm-mips asm-mips64 … linux math-emu net pcmcia scsi video adfs affs autofs autofs4 bfs code cramfs devfs devpts efs ext2 fat hfs hpfs … alpha arm i386 ia64 m68k mips mips64 ppc s390 sh sparc sparc64
Documentation • 存放比较重要的Linux开发文档,遇到问题时,最好先在此处寻找相关的文档 • 这些文档多数是使用OpenDoc,根据源代码中的注释来产生的,类似于javadoc • 现在有人正在编写一本规模更宏大的开放源码内核文档书,请参阅以下网址:http://kernelbook.sourceforge.net • 一个比较值得阅读的文档 • kernel-docs.txt (有关Linux文档的文档)
arch • 本目录下的每个子目录都是一个可以运行Linux的硬件体系 • 每个子目录下都包含了kernel, lib, mm, boot 和其它目录,这些内容都是与具体硬件设备有关的。它们是建立在设备无关代码的“桩”之上的。 • lib目录下包含了高度优化的通用工具例程,例如内存拷贝函数、校验和计算函数等等,它们是学习高效率编程的典范。 • 2.4内核所支持的硬件体系包括: • alpha, arm, i386, ia64, m68k, mips, mips64 • ppc, s390, sh, sparc, sparc64
drivers • 用于实现设备、总线、平台的驱动程序 • 它是Linux内核源代码中最大的部分(100M) • 设备: cdrom, ide, isdn, parport, pcmcia, pnp, sound, telephony, video • 总线:fc4, i2c, nubus, pci, sbus, tc, usb • 平台:acorn, macintosh, s390, sgi • drivers/char:字符设备,例如n_tty.c实现了tty • drivers/block:块设备,例如floppy.c是软盘的驱动程序 • drivers/net:网络设备,包括各种网卡的驱动程序等。
fs • 包括: • 虚拟文件系统(VFS)框架 • 各个实际文件系统的子目录 • Vfs相关文件: • exec.c, binfmt_*.c :可执行程序的加载 • devices.c, blk_dev.c:设备注册与支持 • super.c, filesystems.c:文件系统超级块 • inode.c, dcache.c, namei.c, buffer.c, file_table.c:文件系统管理 • open.c, read_write.c, select.c, pipe.c, fifo.c:基本I/O功能 • fcntl.c, ioctl.c, locks.c, dquot.c, stat.c:其它控制
include • include/asm-* • 与体系结构相关的头文件 • include/linux • 内核和用户应用程序都需要的头文件 • 本目录下的文件中,只供内核使用的部分是用#ifdef来定义的 • #ifdef __KERNEL__ • /* kernel stuff */ • #endif • 其它目录: • math-emu:数学协处理器 • ……
init • 只包括三个文件: • do_mounts.c • main.c • version.c • do_mounts.c:用于实现文件系统的挂载 • version.c:定义了Linux启动的时候所显示的版本信息格式 • main.c:与体系结构无关的启动代码。其中,start_kernel是本目录程序的入口点
ipc • System V进程间通信机制的实现 • 共有四个文件: • sem.c:用于实现信号量 • shm.c:用于实现共享内存 • msg.c:用于实现消息队列 • util.c:当配置Linux内核时禁止使用System V IPC时,由util.c中的“桩”返回错误码
kernel • Linux内核的核心 • sched.c:最主要的内核文件 • 调度器、等待队列、时钟、定时器、任务队列 • 进程控制 • fork.c, exec.c, signal.c, exit.c • acct.c, capability.c, exec_domain.c • 内核模块支持 • kmod.c, ksyms.c, module.c • 其它操作 • time.c, resource.c, dma.c, softirq.c, timer.c • printk.c, info.c, panic.c, sysctl.c, sys.c
lib • 前面提到,内核不能调用标准的C库函数 • 内核自己编写了一套基本的库函数 • 主要的文件: • brlock.c –“Big Reader”自旋锁 • cmdline.c –内核命令行解释程序 • errno.c –错误码的全局定义 • inflate.c –内核解压缩所用到的解压缩程序 • string.c –字符串操作函数 • 通常情况下,如果arch/lib目录下有相同的函数,则使用arch/lib目录下的优化函数 • vsprintf.c –用于实现printk的格式化输出
mm • 分页与交换 • swap.c, swapfile.c (分页设备), swap_state.c (缓存) • vmscan.c –分页策略, kwapd • page_io.c –页面传输的底层操作 • 分配与回收 • slab.c – slab分配器 • page_alloc.c –基于页面的分配器 • vmalloc.c –虚存分配器 • 内存映射 • memory.c –分页、缺页与页表管理 • filemap.c –文件映射 • mmap.c, mremap.c, mlock.c, mprotect.c:内存映射与保护
net • 本目录下存放的是与网络相关的代码 • Core:核心、整个网络部分的框架 • Bridge: 用于实现网桥的代码 • khttpd:内核态的web服务器! • Sched: 用于实现流量控制 • Sunrpc: 用于实现RPC的网络机制 • Unix: 用于实现unix socket • ipv4/ipv6:实现网络层与传输层协议 • ethernet:用于实现以太网协议 • 802:用于实现802.x系列协议 • Bluetooth:蓝牙协议
scripts • script目录包含了以下三类脚本: • 内核配置菜单脚本 • 内核补丁脚本 • 内核文档生成脚本
FAQ(1) • 内核中为什么不能使用printf、malloc? • 内核空间不能调用用户空间的库函数 • 源代码中的volatile是什么意思? • 禁止把变量优化成寄存器变量,一般用于I/O变量 • do{...}while(0) 是什么意思? • 避免宏在不同情况下展开而发生意外错误 • 为什么Red Hat所带的源代码和书上讲的不同? • Linux发行版本所带的源代码往往是被改过的,要看原版请到http://www.kernel.org下载 • 如何统计Linux内核有多少行代码? • find/usr/src/linux-2.x.x-name"*.[chS]" \|xargscat|wc-l
FAQ(2) • 这样写法的结构体是什么意思?structfoo{ unsignedchar a:4, b:4; unsignedint c; unsigned char data[0];}__attribute__((packed)); • a:4,b:4成为“位域”,表示各占4位 • __attribute__((packed))表示禁止使用字对齐,如果不使用改指令,则sizeof(struct foo)的结果是8 • data[0]没有实际意义,只是一个占位符号,用于访问结构体后面的数据
引导过程分析 • 开机自检 • bootsect.s(或BootLoader) • setup.s (或BootLoader) • arch/i386/boot/compressed/head.Sarch/i386/boot/compressed/misc.c • arch/i386/init/main.c, start_kernel() • init线程 init进程
开机自检 • 开机后,计算机工作在实模式下 • 计算机开始执行BIOS中的POST程序 • 检测设备状态 • 检测引导设备 • 将引导设备的第一扇区读入到0x7C00位置处 • 执行0x7C00处的代码
bootsect.s • 将自己从被0x7C00处复制到0x90000处,然后利用一个ljmp的指令,跳到新位置去执行 • 设置栈顶部为0x94000-12,并将其后的12字节用于实现修改了的软盘参数表,以支持2.88M的软盘 • 猜测软盘每磁道的扇区数 • 显示Loading字符 • 一边将setup读入到0x90200 处,一边显示… • 把setup后syssize大小的数据读入到0x10000 • 停止软驱马达工作,显示回车换行符 • 检测bootsect中是否定义了root设备 • 通过一条ljmp指令跳转到setup.s执行
setup.s • 停止软驱工作 • 检测setup尾部的两个签名是否存在,若签名不存在则说明setup没有完全读入,需要把setup读入完毕 • 检测(BIOS所报告的)内存大小、键盘、显卡(调用vedio.s)、硬盘、鼠标、APM等参数,并存放到0x90000-0x901FF内存中,覆盖bootsect • 关闭一切中断,包括NMI • 将从system移动到0x1000处 • 初始化终端描述符表和全局描述符表,进入保护模式,并跳转到0x1000处执行compressed/head.s • 问题:为什么不把system移到0地址处?因为有些笔记本电脑对前4K内存有特殊用途。
head.s • arch/i386/boot/compressed/head.S • 调用misc.c中的decompress_kernel()将内核解压缩到0x100000处 • 跳转到0x100000处执行另一个head.S • arch/i386/boot/head.S • 初始化段寄存器 • 初始化页表、允许换页 • 清空BSS数据段 • 拷贝启动参数 • 调用init/main.c里面的start_kernel()
start_kernel(1) • 输出Linux版本信息 printk(linux_banner) • 设置与体系结构相关的环境 setup_arch() • 提取并分析核心启动参数:从环境变量中读取参数,设置相应标志位等待处理 parse_options() • 使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口trap_init() • 初始化终端处理系统 init_IRQ() • 核心进程调度器初始化 sched_init() • 初始化软中断处理系统 softirq_init() • 时间、定时器初始化:包括读取CMOS时钟、估测主频、初始化定时器中断等 time_init() • 控制台初始化:console_init() • 初始化模块加载器 init_modules() • 剖析器数据结构初始化 profile_init()
start_kernel(2) • 核心Cache(描述Cache信息的Cache)初始化 kmem_cache_init() • 延迟校准:计算CPU的速度 calibrate_delay() • 内存初始化:设置内存上下界和页表项初始值 mem_init() • 创建和设置内部及通用cache:kmem_cache_sizes_init() • 页表缓存初始化 pgtable_cache_init() • fork机制初始化 fork_init() • proc缓存初始化 proc_caches_init() • 虚拟文件系统初始化 vfs_caches_init() • 页缓存初始化 page_cache_init() • 信号机制初始化 signals_init() • /proc文件系统初始化 proc_root_init() • 进程间通信机制初始化 ipc_init() • 检查CPU的bug check_bugs() • 创建第一个核心线程用于执行init(),原线程调用idle并等待调度rest_init()
init() • 初始化外设 do_basic_setup() • 释放启动内存段 free_initmem() • 打开/dev/console,把stdin,stdout,stderr都重定向到控制台 • 如果启动参数中用init=xxx指定了第一个用户进程,则执行它,否则依次搜索如下程序: • /sbin/init • /etc/init • /bin/init • /bin/sh
do_basic_setup() • 总线初始化 pci_init()等 • 创建事件管理核心线程keventd start_context_thread() • 通过do_initcalls()依次调用使用__initcall或者module_init宏定义的函数,这些函数的功能主要包括 • 创建kflushd核心线程:用于清理被写过的内存缓冲区 • 创建kupdate核心线程:定时更新的内容超级块和inode表 • 设置并启动核心换页线程kswapd • 设备初始化:包括并口、字符设备、块设备 、SCSI设备、网络设备、磁盘初始化及分区检查等 • 文件系统初始化
进程调度策略与优先级 • SCHED_FIFO: 适用于实时进程,先来先服务 • SCHED_RR:适用于实时进程,时间片轮转算法 • SCHED_OTHER:适用于一般进程,具有动态优先级的时间片轮转算法 • 用户可以用nice或者renice命令指定进程的nice值(-20~19之间,默认为0) • 每次进程获得的时间片数量为21-nice • nice越小,时间片越多
schedule() • 将prev指针指向当前正在运行的进程。 • 如果prev进程是用尽了时间片的SCHED_RR进程,那么,把它的counter值置为priority的值并将它移到就绪队列末尾。 • 如果当前的调度是因为prev进程需要等待资源而发生的,那么检查唤醒的信号是否已经到达该进程。若到达,则继续运行。 • 遍历就绪队列中的所有进程,逐一计算各任务的goodness值,最后选出goodness值最大的任务,并将它的goodness值记为c • 如果循环结束后,得到c的值为0,则说明运行队列中的所有进程的goodness值都为0,也就是说所有进程都已经用完了它的时间片。在这种情况下,schedule要重新把counter初始化为priority。此步完成后重复上一步,选出一个进程。 • 如果选出的进程不同于prev,则挂起原来的进程,运行新的进程。此时调用switch_to来进行上下文切换。
goodness() • 每次调度时,计算各个进程的weight值,weight值最大的进程,获得CPU时间 • counter是剩余时间片计数器
驱动程序的接口与调用 • 驱动程序的实现必须符合OS所定义的接口 • OS调用驱动程序是通过函数指针来实现的 • 在OS源代码中定义: • int (* read)(char *buf, int length); • 在驱动程序中定义: • int dev_read(char *buf, int length) { ……} • send=&dev_read; • 在OS源代码中调用 • rc=(*read)(buffer, 500); • 调用(*read)实际上就是调用了dev_read
几点提示 • 参考Linux源代码中包含的大量驱动程序 • 利用skeleton实现 • 很多驱动程序的目录下都有名为skeleton的程序 • 这是该类驱动程序的编写框架 • 编写新的驱动程序的时候,应该根据该框架进行 • 对于初学者而言,修改一个类似的驱动程序比写一个新的驱动程序更简单 • 驱动程序的阅读和编写应该重点把握以下三方面内容: • 读、写、中断处理