1 / 52

Linux 内核分析入门

Linux 内核分析入门. 西安交通大学 李思 2004 年 8 月 26 日. 主要内容. 有关 Linux 内核的基础知识 实模式与保护模式 用户空间与内核空间 Linux 内核源代码导读 工具与策略 内核源码目录结构 实例分析 Linux 启动代码分析 Linux 进程调度代码分析 Linux 设备驱动程序设计概述. 1. Linux 内核的发展. 内核源代码的行数. 2. 有关 Linux 内核的基础知识. (左移四位). 16 位段地址. 软件地址:. 16 位段地址. :. 16 位段内偏移. 物理地址:. 16 位段内偏移.

Download Presentation

Linux 内核分析入门

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Linux内核分析入门 西安交通大学 李思 2004年8月26日

  2. 主要内容 • 有关Linux内核的基础知识 • 实模式与保护模式 • 用户空间与内核空间 • Linux内核源代码导读 • 工具与策略 • 内核源码目录结构 • 实例分析 • Linux启动代码分析 • Linux进程调度代码分析 • Linux设备驱动程序设计概述

  3. 1. Linux内核的发展

  4. 内核源代码的行数

  5. 2. 有关Linux内核的基础知识

  6. (左移四位) 16位段地址 软件地址: 16位段地址 : 16位段内偏移 物理地址: 16位段内偏移 + = 20位物理地址 实模式 • BIOS 、Dos都工作在实模式下 • 实模式的缺点: • 寻址空间只有1M • 不能实现虚拟存储器,不能实现进程的隔离

  7. 保护模式 • Linux初始化完成后工作在保护模式 • 保护模式的优点: • 寻址空间可达4G,虚存,进程相互隔离 • 寻址方式: • 以32位段寄存器的值(段号+)作为索引查描述符表(段表),找到对应的描述符(基址、长度等) • 检查32位的偏移量是否超过段长度 • 检查权限是否正确 • 物理地址 = 基址 + 偏移量

  8. 两种模式的比较 实模式 保护模式

  9. 用户空间与内核空间 • 用户空间:一般应用程序所访问到的范围。它的特权级别低,不能直接访问底层的资源,且每个进程的用户地址空间是相互独立的。用户空间的进程是可以被内核进程所中断的。 • 内核空间:内核与内核模块所访问到的范围。它的特权级别最高,可以直接访问底层资源。内核地址空间只有1个。一般情况下,内核进程不能被任何用户进程所打断。 • 常用的用户空间与内核空间通信方式 • 通过设备文件 • 使用/proc文件系统 • 使用netlink套接字通信 • 通过系统调用传递数据

  10. 用户与内核的接口

  11. LKM • LKM = Loadable Kernel Module, 可加载的内核模块。当不发生歧义时,简称为内核模块,或者模块。 • LKM工作在内核空间下,一般看作是内核的一部分 • LKM是一种可加载/卸载的内核功能块 • LKM可以直接使用内核中的全局变量、调用内核中导出的其它函数 • LKM与静态编译到内核中的功能模块,在地位上是完全平等的 • 驱动程序一般使用LKM来实现 • 一般开发者的原则: 尽量多写模块,少改内核

  12. Linux内核源代码概览

  13. 读核工具 • Source Insight • 只有Windows版 • 在Linux下可以用wine来运行 • Source Navigator • 类似于前者,但有Linux版本 • LXR引擎:Linux Cross Reference • 根据内核源代码创建HTML页面 • 可以随意搜索标号的定义及其引用 • http://lxr.linux.no

  14. 读核策略 • 版本选择:可先读低版本,再读高版本 • 纵向与横向 • 纵向:按执行顺序阅读 • 横向:按功能模块来阅读 • 精读与泛读 • 全局泛读,局部精读 • 对于一些一时无法理解的部分,如果不是研究的重点,则要果断跳过,但切不可贪多求快! • 参考资料的合理使用 • 应该积极利用各种参考资料 • 不要过分迷信参考资料 • 要注意内核版本的区别!

  15. Linux内核各部分关系

  16. 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

  17. Documentation • 存放比较重要的Linux开发文档,遇到问题时,最好先在此处寻找相关的文档 • 这些文档多数是使用OpenDoc,根据源代码中的注释来产生的,类似于javadoc • 现在有人正在编写一本规模更宏大的开放源码内核文档书,请参阅以下网址:http://kernelbook.sourceforge.net • 一个比较值得阅读的文档 • kernel-docs.txt (有关Linux文档的文档)

  18. arch • 本目录下的每个子目录都是一个可以运行Linux的硬件体系 • 每个子目录下都包含了kernel, lib, mm, boot 和其它目录,这些内容都是与具体硬件设备有关的。它们是建立在设备无关代码的“桩”之上的。 • lib目录下包含了高度优化的通用工具例程,例如内存拷贝函数、校验和计算函数等等,它们是学习高效率编程的典范。 • 2.4内核所支持的硬件体系包括: • alpha, arm, i386, ia64, m68k, mips, mips64 • ppc, s390, sh, sparc, sparc64

  19. 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:网络设备,包括各种网卡的驱动程序等。

  20. 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:其它控制

  21. include • include/asm-* • 与体系结构相关的头文件 • include/linux • 内核和用户应用程序都需要的头文件 • 本目录下的文件中,只供内核使用的部分是用#ifdef来定义的 • #ifdef __KERNEL__ • /* kernel stuff */ • #endif • 其它目录: • math-emu:数学协处理器 • ……

  22. init • 只包括三个文件: • do_mounts.c • main.c • version.c • do_mounts.c:用于实现文件系统的挂载 • version.c:定义了Linux启动的时候所显示的版本信息格式 • main.c:与体系结构无关的启动代码。其中,start_kernel是本目录程序的入口点

  23. ipc • System V进程间通信机制的实现 • 共有四个文件: • sem.c:用于实现信号量 • shm.c:用于实现共享内存 • msg.c:用于实现消息队列 • util.c:当配置Linux内核时禁止使用System V IPC时,由util.c中的“桩”返回错误码

  24. 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

  25. lib • 前面提到,内核不能调用标准的C库函数 • 内核自己编写了一套基本的库函数 • 主要的文件: • brlock.c –“Big Reader”自旋锁 • cmdline.c –内核命令行解释程序 • errno.c –错误码的全局定义 • inflate.c –内核解压缩所用到的解压缩程序 • string.c –字符串操作函数 • 通常情况下,如果arch/lib目录下有相同的函数,则使用arch/lib目录下的优化函数 • vsprintf.c –用于实现printk的格式化输出

  26. 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:内存映射与保护

  27. net • 本目录下存放的是与网络相关的代码 • Core:核心、整个网络部分的框架 • Bridge: 用于实现网桥的代码 • khttpd:内核态的web服务器! • Sched: 用于实现流量控制 • Sunrpc: 用于实现RPC的网络机制 • Unix: 用于实现unix socket • ipv4/ipv6:实现网络层与传输层协议 • ethernet:用于实现以太网协议 • 802:用于实现802.x系列协议 • Bluetooth:蓝牙协议

  28. scripts • script目录包含了以下三类脚本: • 内核配置菜单脚本 • 内核补丁脚本 • 内核文档生成脚本

  29. 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

  30. 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]没有实际意义,只是一个占位符号,用于访问结构体后面的数据

  31. 实例分析1: 启动代码

  32. 引导过程分析 • 开机自检 • 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进程

  33. 开机自检 • 开机后,计算机工作在实模式下 • 计算机开始执行BIOS中的POST程序 • 检测设备状态 • 检测引导设备 • 将引导设备的第一扇区读入到0x7C00位置处 • 执行0x7C00处的代码

  34. bootsect.s • 将自己从被0x7C00处复制到0x90000处,然后利用一个ljmp的指令,跳到新位置去执行 • 设置栈顶部为0x94000-12,并将其后的12字节用于实现修改了的软盘参数表,以支持2.88M的软盘 • 猜测软盘每磁道的扇区数 • 显示Loading字符 • 一边将setup读入到0x90200 处,一边显示… • 把setup后syssize大小的数据读入到0x10000 • 停止软驱马达工作,显示回车换行符 • 检测bootsect中是否定义了root设备 • 通过一条ljmp指令跳转到setup.s执行

  35. setup.s • 停止软驱工作 • 检测setup尾部的两个签名是否存在,若签名不存在则说明setup没有完全读入,需要把setup读入完毕 • 检测(BIOS所报告的)内存大小、键盘、显卡(调用vedio.s)、硬盘、鼠标、APM等参数,并存放到0x90000-0x901FF内存中,覆盖bootsect • 关闭一切中断,包括NMI • 将从system移动到0x1000处 • 初始化终端描述符表和全局描述符表,进入保护模式,并跳转到0x1000处执行compressed/head.s • 问题:为什么不把system移到0地址处?因为有些笔记本电脑对前4K内存有特殊用途。

  36. 启动过程的代码流

  37. setup.s之后的内存状况

  38. 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()

  39. 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()

  40. 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()

  41. init() • 初始化外设 do_basic_setup() • 释放启动内存段 free_initmem() • 打开/dev/console,把stdin,stdout,stderr都重定向到控制台 • 如果启动参数中用init=xxx指定了第一个用户进程,则执行它,否则依次搜索如下程序: • /sbin/init • /etc/init • /bin/init • /bin/sh

  42. do_basic_setup() • 总线初始化 pci_init()等 • 创建事件管理核心线程keventd start_context_thread() • 通过do_initcalls()依次调用使用__initcall或者module_init宏定义的函数,这些函数的功能主要包括 • 创建kflushd核心线程:用于清理被写过的内存缓冲区 • 创建kupdate核心线程:定时更新的内容超级块和inode表 • 设置并启动核心换页线程kswapd • 设备初始化:包括并口、字符设备、块设备 、SCSI设备、网络设备、磁盘初始化及分区检查等 • 文件系统初始化

  43. 5. 实例分析2: 进程调度

  44. 进程调度策略与优先级 • SCHED_FIFO: 适用于实时进程,先来先服务 • SCHED_RR:适用于实时进程,时间片轮转算法 • SCHED_OTHER:适用于一般进程,具有动态优先级的时间片轮转算法 • 用户可以用nice或者renice命令指定进程的nice值(-20~19之间,默认为0) • 每次进程获得的时间片数量为21-nice • nice越小,时间片越多

  45. schedule() • 将prev指针指向当前正在运行的进程。 • 如果prev进程是用尽了时间片的SCHED_RR进程,那么,把它的counter值置为priority的值并将它移到就绪队列末尾。 • 如果当前的调度是因为prev进程需要等待资源而发生的,那么检查唤醒的信号是否已经到达该进程。若到达,则继续运行。 • 遍历就绪队列中的所有进程,逐一计算各任务的goodness值,最后选出goodness值最大的任务,并将它的goodness值记为c • 如果循环结束后,得到c的值为0,则说明运行队列中的所有进程的goodness值都为0,也就是说所有进程都已经用完了它的时间片。在这种情况下,schedule要重新把counter初始化为priority。此步完成后重复上一步,选出一个进程。 • 如果选出的进程不同于prev,则挂起原来的进程,运行新的进程。此时调用switch_to来进行上下文切换。

  46. goodness() • 每次调度时,计算各个进程的weight值,weight值最大的进程,获得CPU时间 • counter是剩余时间片计数器

  47. 6. Linux驱动程序设计概述

  48. 驱动程序的地位

  49. 驱动程序的接口与调用 • 驱动程序的实现必须符合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

  50. 几点提示 • 参考Linux源代码中包含的大量驱动程序 • 利用skeleton实现 • 很多驱动程序的目录下都有名为skeleton的程序 • 这是该类驱动程序的编写框架 • 编写新的驱动程序的时候,应该根据该框架进行 • 对于初学者而言,修改一个类似的驱动程序比写一个新的驱动程序更简单 • 驱动程序的阅读和编写应该重点把握以下三方面内容: • 读、写、中断处理

More Related