LINUX内核模块编程指南
- 格式:pdf
- 大小:2.65 MB
- 文档页数:71
Linux设备驱动程序原理及框架-内核模块入门篇内核模块介绍应用层加载模块操作过程内核如何支持可安装模块内核提供的接口及作用模块实例内核模块内核模块介绍Linux采用的是整体式的内核结构,这种结构采用的是整体式的内核结构,采用的是整体式的内核结构的内核一般不能动态的增加新的功能。
为此,的内核一般不能动态的增加新的功能。
为此,Linux提供了一种全新的机制,叫(可安装) 提供了一种全新的机制,可安装) 提供了一种全新的机制模块” )。
利用这个机制“模块”(module)。
利用这个机制,可以)。
利用这个机制,根据需要,根据需要,在不必对内核重新编译链接的条件将可安装模块动态的插入运行中的内核,下,将可安装模块动态的插入运行中的内核,成为内核的一个有机组成部分;成为内核的一个有机组成部分;或者从内核移走已经安装的模块。
正是这种机制,走已经安装的模块。
正是这种机制,使得内核的内存映像保持最小,的内存映像保持最小,但却具有很大的灵活性和可扩充性。
和可扩充性。
内核模块内核模块介绍可安装模块是可以在系统运行时动态地安装和卸载的内核软件。
严格来说,卸载的内核软件。
严格来说,这种软件的作用并不限于设备驱动,并不限于设备驱动,例如有些文件系统就是以可安装模块的形式实现的。
但是,另一方面,可安装模块的形式实现的。
但是,另一方面,它主要用来实现设备驱动程序或者与设备驱动密切相关的部分(如文件系统等)。
密切相关的部分(如文件系统等)。
课程内容内核模块介绍应用层加载模块操作过程内核如何支持可安装模块内核提供的接口及作用模块实例内核模块应用层加载模块操作过程内核引导的过程中,会识别出所有已经安装的硬件设备,内核引导的过程中,会识别出所有已经安装的硬件设备,并且创建好该系统中的硬件设备的列表树:文件系统。
且创建好该系统中的硬件设备的列表树:/sys 文件系统。
(udev 服务就是通过读取该文件系统内容来创建必要的设备文件的。
)。
linux module的用法
Linux模块是一种可以动态加载到Linux内核中以扩展其功能的软件组件。
它们通常用于添加新的驱动程序、文件系统或其他内核功能。
下面我将从多个角度来介绍Linux模块的用法。
首先,要编写一个Linux模块,你需要具备一定的C语言编程知识。
一个基本的Linux模块包括初始化函数和清理函数。
初始化函数在模块加载时被调用,而清理函数在模块被卸载时被调用。
你需要使用特定的宏和数据结构来定义模块的初始化和清理函数,以及模块的许可证和作者信息。
其次,编译模块需要使用Linux内核源代码中的构建系统。
你需要确保已经安装了正确版本的内核头文件和构建工具。
然后,你可以编写一个Makefile来编译你的模块。
在Makefile中,你需要指定内核源代码的路径,并使用特定的命令来编译模块。
一旦你编译好了你的模块,你可以使用insmod命令将其加载到内核中。
加载模块后,你可以使用lsmod命令来查看已加载的模块列表。
你还可以使用modinfo命令来查看模块的信息,包括作者、描述和许可证等。
当你不再需要模块时,你可以使用rmmod命令将其从内核中卸载。
卸载模块后,你可以使用dmesg命令来查看内核日志,以确保
模块已经成功卸载。
总的来说,Linux模块的用法涉及到编写模块代码、编译模块、加载模块以及卸载模块等步骤。
掌握了这些基本的用法,你就可以
开始开发自己的Linux内核模块了。
希望这些信息能够帮助你更好
地理解Linux模块的用法。
linux 模块编译步骤(原)本文将直接了当的带你进入linux的模块编译。
当然在介绍的过程当中,我也会添加一些必要的注释,以便初学者能够看懂。
之所以要写这篇文章,主要是因为从书本上学的话,可能要花更长的时间才能学会整个过程,因为看书的话是一个学习过程,而我这篇文章更像是一个培训。
所以实践性和总结性更强。
通过本文你将会学到编译一个模块和模块makefile的基本知识。
以及加载(卸载)模块,查看系统消息的一些知识;声明:本文为初学者所写,如果你已经是一个linux模块编译高手,还请指正我文章中的错误和不足,谢谢第一步:准备源代码首先我们还是要来编写一个符合linux格式的模块文件,这样我们才能开始我们的模块编译。
假设我们有一个源文件mymod.c。
它的源码如下:mymodules.c1. #include <linux/module.h> /* 引入与模块相关的宏*/2. #include <linux/init.h> /* 引入module_init() module_exit()函数*/3. #include <linux/moduleparam.h> /* 引入module_param() */45. MODULE_AUTHOR("Yu Qiang");6. MODULE_LICENSE("GPL");78. static int nbr = 10;9. module_param(nbr, int, S_IRUGO);10.11. static int __init yuer_init(void)12.{13. int i;14. for(i=0; i<nbr; i++)15. {16. printk(KERN_ALERT "Hello, How are you. %d\n", i);17. }18. return 0;19.}20.21.static void __exit yuer_exit(void)22.{23. printk(KERN_ALERT"I come from yuer's module, I have been unlad.\n");24.}25.26. module_init(yuer_init);27. module_exit(yuer_exit);我们的源文件就准备的差不多了,这就是一个linux下的模块的基本结构。
LINUX内核模块编译步骤编译Linux内核模块主要包括以下步骤:1.获取源代码2.配置内核进入源代码目录并运行make menuconfig命令来配置内核。
该命令会打开一个文本菜单,其中包含许多内核选项。
在这里,你可以配置内核以适应特定的硬件要求和预期的功能。
你可以选择启用或禁用各种功能、设备驱动程序和文件系统等。
配置完成后,保存并退出。
3. 编译内核(make)运行make命令开始编译内核。
这将根据你在上一步中进行的配置生成相应的Makefile,然后开始编译内核。
编译的过程可能需要一些时间,请耐心等待。
4.安装模块编译完成后,运行make modules_install命令将编译好的模块安装到系统中。
这些模块被安装在/lib/modules/<kernel-version>/目录下。
5.安装内核运行make install命令来安装编译好的内核。
该命令会将内核映像文件(通常位于/arch/<architecture>/boot/目录下)复制到/boot目录,并更新系统引导加载程序(如GRUB)的配置文件。
6.更新GRUB配置文件运行update-grub命令来更新GRUB引导加载程序的配置文件。
这将确保新安装的内核在下次启动时可用。
7.重启系统安装完成后,通过重启系统来加载新的内核和模块。
在系统启动时,GRUB将显示一个菜单,你可以选择要启动的内核版本。
8.加载和卸载内核模块现在,你可以使用insmod命令来加载内核模块。
例如,运行insmod hello.ko命令来加载名为hello.ko的模块。
加载的模块位于/lib/modules/<kernel-version>/目录下。
如果你想卸载一个已加载的内核模块,可以使用rmmod命令。
例如,运行rmmod hello命令来卸载已加载的hello模块。
9.编写和编译模块代码要编写一个内核模块,你需要创建一个C文件,包含必要的模块代码。
二、Linux 汇编语法格式绝大多数 Linux 程序员以前只接触过DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。
但在 Unix 和 Linux 系统中,更多采用的还是 AT&T 格式,两者在语法格式上有着很大的不同:1.在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。
例如:2.在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。
例如:3.AT&T 和 Intel 格式中的源操作数和目标操作数的位置正好相反。
在Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。
例如:4.在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr" 等前缀来表示的。
例如:5.在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上'*'作为前缀,而在 Intel 格式中则不需要。
6.远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为"ljump" 和 "lcall",而在 Intel 汇编格式中则为 "jmp far" 和 "call far",即:7.与之相应的远程返回指令则为:8.在 AT&T 汇编格式中,内存操作数的寻址方式是section:disp(base, index, scale)而在 Intel 汇编格式中,内存操作数的寻址方式为:section:[base + index*scale + disp]由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:disp + base + index * scale下面是一些内存操作数的例子:三、Hello World!真不知道打破这个传统会带来什么样的后果,但既然所有程序设计语言的第一个例子都是在屏幕上打印一个字符串 "Hello World!",那我们也以这种方式来开始介绍 Linux 下的汇编语言程序设计。
Linux内核模块开发(简单)Linux系统为应⽤程序提供了功能强⼤且容易扩展的API,但在某些情况下,这还远远不够。
与硬件交互或进⾏需要访问系统中特权信息的操作时,就需要⼀个内核模块。
Linux内核模块是⼀段编译后的⼆进制代码,直接插⼊Linux内核中,在 Ring 0(x86–64处理器中执⾏最低和受保护程度最低的执⾏环)上运⾏。
这⾥的代码完全不受检查,但是运⾏速度很快,可以访问系统中的所有内容。
Intel x86架构使⽤了4个级别来标明不同的特权级。
Ring 0实际就是内核态,拥有最⾼权限。
⽽⼀般应⽤程序处于Ring 3状态--⽤户态。
在Linux中,还存在Ring 1和Ring 2两个级别,⼀般归属驱动程序的级别。
在Windows平台没有Ring 1和Ring 2两个级别,只⽤Ring 0内核态和Ring 3⽤户态。
在权限约束上,⾼特权等级状态可以阅读低特权等级状态的数据,例如进程上下⽂、代码、数据等等,但反之则不可。
Ring 0最⾼可以读取Ring 0-3所有的内容,Ring 1可以读Ring 1-3的,Ring 2以此类推,Ring 3只能读⾃⼰的数据。
1. 为什么要开发内核模块编写Linux内核模块并不是因为内核太庞⼤⽽不敢修改。
直接修改内核源码会导致很多问题,例如:通过更改内核,你将⾯临数据丢失和系统损坏的风险。
内核代码没有常规Linux应⽤程序所拥有的安全防护机制,如果内核发⽣故障,将锁死整个系统。
更糟糕的是,当你修改内核并导致错误后,可能不会⽴即表现出来。
如果模块发⽣错误,在其加载时就锁定系统是最好的选择,如果不锁定,当你向模块中添加更多代码时,你将会⾯临失控循环和内存泄漏的风险,如果不⼩⼼,它们会随着计算机继续运⾏⽽持续增长,最终,关键的存储器结构甚⾄缓冲区都可能被覆盖。
编写内核模块时,基本是可以丢弃传统的应⽤程序开发范例。
除了加载和卸载模块之外,你还需要编写响应系统事件的代码(⽽不是按顺序模式执⾏的代码)。
⼯作模式⼯作性质层次权限影响竞态运⾏⽅式应⽤程序USR 模式策略性⽤户层低局部局部主动内核模块SVC 模式功能性内核层⾼全局全局被挡Linux 内核模块1、什么是内核模块?内核模块是Linux 提供的⼀种机制,允许在内核运⾏时动态加载进内核中,具有两个特点: 1)内核模块本⾝不编译⼊内核映像,有效控制缩减内核镜像⼤⼩ 2)内核模块⼀旦被加载,他就和内核中的其他部分完全⼀样2、为什么需要内核模块?如果在内核编译时把所有的功能都编译进去,就会导致内核很⼤,⽽且要往内核中添加或删除功能时必须重新编译内核⽐如在Ubuntu 在通⽤PC 平台上,预先⽆法知道需要什么设备,就不知道预先编译什么驱动。
3、内核模块和应⽤程序的区别4、内核模块的基本构成|——两个函数(⼀般需要)| |——模块初始化(加载)函数:当内核模块加载进内核的时候,做⼀些准备⼯作| |——模块卸载函数:回收、清理资源||——授权(许可证声明)(必须):Linux 内核受GPL (General Public License )授权约束|——模块参数(可选):模块被加载时可以被传递给它的值,本⾝对应模块内的全局变量|——模块导出符号(可选)|——模块信息说明(可选)5、模块加载(初始化)函数⼀般以 __init 标识声明函数命名规则 xxx_init xxx 设备名 init 功能名(初始化)函数形式:static ini __init xxx_init(void ){/* 初始化代码* 返回值: 成功:0 失败:负数,绝对值是错误码* 应⽤层得到的返回值是-1,错误码保存到errno (每个进程有⼀个); 标准化errno.h 已经明确定义linux/errno.h */}注册⽅式: module_init(x); x 为模块初始化函数的⾸地址 6、模块卸载函数⼀般以 __exit 标识声明函数命名规则 xxx_exit xxx 设备名 exit 功能名(卸载)static ini __exit xxx_exit(void ){/* 释放代码 */}注册⽅式: module_exit(x); x为模块卸载函数的⾸地址7、模块许可证声明MODULE_LICENSE(_license) //_license就是授权名称的字符串//"GPL" [GNU Public License v2 or later]//"GPL v2" [GNU Public License v2]//"GPL and additional rights" [GNU Public License v2 rights and more]//"Dual BSD/GPL" [GNU Public License v2 or BSD license choice]//"Dual MIT/GPL" [GNU Public License v2 or MIT license choice]//"Dual MPL/GPL" [GNU Public License v2 or Mozilla license choice]8、模块声明与描述在Linux内核模块中,我们可以⽤MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别来声明模块的作者、描述、版本、设备表和别名,例如:MODULE_AUTHOR(author);MODULE_DESCRIPTION(description);MODULE_VERSION(version_string);MODULE_DEVICE_TABLE(table_info);MODULE_ALIAS(alternate_name);对于USB、PCI等设备驱动,通常会创建⼀个MODULE_DEVICE_TABLE,表明该驱动模块⽀持的设备,如:/* 对应此驱动的设备列表 */static struct usb_device_id skel_table [ ] = {{USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) }, { } /* 表结束 */}};MODULE_DEVICE_TABLE (usb, skel_table);9、模块参数:在加载模块时,可以给模块传参头⽂件 linux/moduleparam.hA、传递普通变量module_param(name, type, perm);声明内核模块参数/*name - 接收参数的变量名type - 变量类型 Standard types are: byte, short, ushort, int, uint, long, ulong charp: a character pointer bool: a bool, values 0/1, y/n, Y/N. invbool: the above, only sense-reversed (N = true)perm - 权限 头⽂件 linux/stat.h #define S_IRWXUGO (S_IRWXU|S_IRWXG|S_IRWXO) #define S_IALLUGO (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO) #define S_IRUGO (S_IRUSR|S_IRGRP|S_IROTH) #define S_IWUGO (S_IWUSR|S_IWGRP|S_IWOTH) #define S_IXUGO (S_IXUSR|S_IXGRP|S_IXOTH)*/范例:int i = 0;module_param(i, int, 0644);运⾏:# insmod xxx.ko i=10B、传递数组参数module_param_array(name, type, nump, perm)/*声明内核模块数组参数name - 数组名type - 数组成员类型nump – ⼀个指向保存数组长度的整型变量的指针perm - 权限*/范例:int arr[] = {1,2,3,4,5,6};int len=0;module_param(arr, int, &len, 0644);运⾏:# insmod xxx.ko arr=1,2,3,4,5C、传递字符串参数module_param_string(name, string, len, perm)/*声明内核模块字符串参数name - 字符串缓存的外部名(传⼊变量名)string - 字符串缓存的内部名nump - 数组的数量perm - 权限*/范例:char insidestr[] = "hello world";module_param(extstr, insidestr, szieof(insidestr), 0644);运⾏:# insmod xxx.ko extstr="hello"10、编译内核模块如果⼀个内核模块要加载到某个内核中运⾏,则这个模块必须使⽤编译该内核镜像的源码进⾏编译,否则运⾏时会出错A、头⽂件(语法问题)B、编译结果(最主要影响)编译时符号表(只在编译时使⽤)运⾏时内核符号表# cat /proc/kallsyms 运⾏时内核符号表C、编译系统⽰例Makefile:# 内核模块的Makefile(模块源码在内核源码外,且内核先编译)# 1、找内核的Makefile# 2、内核的Makefile找内核模块的Makeifle内核模块的Makeifle定义要编译对象ifneq ($(KERNELRELEASE),)#要编译对象表⽰把demo.c编译成demo.ko obj-m = demo.oelse#内核源码⽬录KERNELDIR := /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modulesendifclean: rm -rf .tmp_versions Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod.cKERNELRELEASE 是在内核源码的顶层Makefile中定义的⼀个变量,在第⼀次读取执⾏此Makefile时,KERNELRELEASE没有被定义,所以make将读取执⾏else之后的内容。
《Linux高级系统编程》教学教案一、教学目标1. 让学生掌握Linux系统编程的基本概念和原理。
2. 培养学生熟练使用Linux系统编程API的能力。
3. 使学生了解Linux系统编程的高级主题和技巧。
4. 培养学生解决实际问题的能力,提高他们在Linux环境下的软件开发水平。
二、教学内容1. Linux系统编程概述讲解Linux系统编程的基本概念、特点和优势。
2. 文件I/O操作介绍Linux文件I/O模型,讲解文件的打开、关闭、读写、同步等操作。
3. 进程管理讲解Linux进程的概念、创建、终止、进程间通信等知识。
4. 线程管理介绍Linux线程的基本概念、创建、同步、互斥等知识。
5. 高级I/O操作讲解Linux高级I/O操作,如异步I/O、直接I/O、内存映射I/O等。
三、教学方法1. 讲授法:讲解基本概念、原理和知识点。
2. 案例教学法:通过实际案例让学生掌握编程技巧和方法。
3. 实验教学法:安排实验课程,让学生亲自动手实践,提高实际操作能力。
四、教学环境1. 教室环境:投影仪、计算机、网络等。
2. 实验环境:装有Linux操作系统的计算机、网络等。
五、教学评估1. 课堂问答:检查学生对课堂知识的理解和掌握程度。
2. 实验报告:评估学生在实验过程中的动手能力和解决问题能力。
3. 课程作业:检查学生对课程知识点的综合运用能力。
4. 期末考试:全面评估学生对本门课程的掌握程度。
六、信号处理1. 信号基本概念讲解信号的定义、作用和信号处理的基本方法。
2. 信号处理函数介绍Linux信号处理函数,如signal(), rse(), sigaction()等。
3. 信号在进程和线程中的处理讲解信号在进程和线程中的传播和处理机制。
七、同步与互斥1. 同步与互斥基本概念讲解同步与互斥的概念、作用和应用场景。
2. 互斥锁介绍Linux互斥锁的使用,如pthread_mutex_lock(), pthread_mutex_unlock()等。
linux 编译ko流程在Linux下,编译内核模块(.ko 文件)通常涉及以下步骤。
这些步骤可能会根据具体的内核版本和构建环境有所不同,但基本流程是相似的。
准备源代码:获取内核源代码,通常可以从官方网站、发行版仓库或Git仓库获取。
将源代码解压到适当的位置。
设置编译环境:安装必要的编译工具,如make、gcc 等。
配置交叉编译环境(如果需要)。
配置内核:进入内核源代码目录。
运行make menuconfig 或make defconfig(或其他配置命令)来配置内核选项。
在这里,你可以选择要编译为模块的内核特性。
保存并退出配置工具。
编译内核模块:在内核源代码目录下,运行make 命令来编译内核。
如果只需要编译特定模块,可以使用make M=$(PWD) modules,其中$(PWD) 是当前目录的路径。
编译完成后,生成的.ko 文件通常位于arch/<体系结构>/boot 或drivers/<模块目录> 下。
安装内核模块:将生成的.ko 文件复制到/lib/modules/$(uname -r)/kernel/ 或/lib/modules/$(uname -r)/extra/ 目录下。
运行depmod -a 来更新模块依赖。
(可选)创建软链接,以便在其他内核版本下也能加载模块。
加载和测试模块:使用insmod 或modprobe 命令加载模块。
使用lsmod 命令检查模块是否已加载。
使用dmesg 或/var/log/messages 查看加载过程中的消息,以确认模块是否成功加载。
运行任何必要的测试或验证模块功能。
卸载模块:使用rmmod 命令卸载模块。
请注意,具体的步骤可能会因内核版本、架构和特定需求而有所不同。
在编译内核模块之前,建议仔细阅读相关文档和内核源代码中的说明。
Linux的内核编译和内核模块的管理一、内核的介绍内核室操作系统的最重要的组件,用来管理计算机的所有软硬件资源,以及提供操作系统的基本能力,RED hatenterpriselinux的许多功能,比如软磁盘整列,lvm,磁盘配额等都是由内核来提供。
1.1内核的版本与软件一样内核也会定义版本的信息,以便让用户可以清楚的辨认你用得是哪个内核的一个版本,linux内核以以下的的语法定义版本的信息MAJOR.MINOR.RELEASE[-CUSTOME]MAJOR:主要的版本号MINOR:内核的次版本号,如果是奇数,表示正在开发中的版本,如果是偶数,表示稳定的版本RELEASE:修正号,代表这个事第几次修正的内核CUSTOME 这个是由linux产品商做定义的版本编号。
如果想要查看内核的版本使用uname 来查看语法#uname [选项]-r --kernel-release 只查看目前的内核版本号码-s --kernel-name 支持看内核名称、-n --nodename 查看当前主机名字-v --kernel-version 查看当前内核的版本编译时间-m --machine 查看内核机器平台名称-p --processor 查看处理器信息-I --hard-platform 查看硬件平台信息-o --operating-system 查看操作系统的名称-a 查看所有1.2内核的组件内核通常会以镜像文件的类型来存储在REDHAT ENTERPRISE LINUX 中,当你启动装有REDHAT ENTERPRISE linux的系统的计算机时,启动加载器bootloader 程序会将内核镜像文件直接加载到程序当中,已启动内核与整个操作系统一般来说,REDHAT ENTERPRISE LINUX 会把内核镜像文件存储在/boot/目录中,文件名称vmlinuz-version或者vmlinux-version 其中version就是内的版本号内核模块组成linux内核的第二部分是内核模块,或者单独成为内核模块。
Linux内核编译内幕详解内核,是一个操作系统的核心。
它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。
Linux的一个重要的特点就是其源代码的公开性,所有的内核源程序都可以在/usr/src/l inux下找到,大部分应用软件也都是遵循GPL而设计的,你都可以获取相应的源程序代码。
全世界任何一个软件工程师都可以将自己认为优秀的代码加入到其中,由此引发的一个明显的好处就是Linux修补漏洞的快速以及对最新软件技术的利用。
而Linux的内核则是这些特点的最直接的代表。
想象一下,拥有了内核的源程序对你来说意味着什么?首先,我们可以了解系统是如何工作的。
通过通读源代码,我们就可以了解系统的工作原理,这在Windows下简直是天方夜谭。
其次,我们可以针对自己的情况,量体裁衣,定制适合自己的系统,这样就需要重新编译内核。
在Windows下是什么情况呢?相信很多人都被越来越庞大的Windows整得莫名其妙过。
再次,我们可以对内核进行修改,以符合自己的需要。
这意味着什么?没错,相当于自己开发了一个操作系统,但是大部分的工作已经做好了,你所要做的就是要增加并实现自己需要的功能。
在Windows下,除非你是微软的核心技术人员,否则就不用痴心妄想了。
内核版本号由于Linux的源程序是完全公开的,任何人只要遵循GPL,就可以对内核加以修改并发布给他人使用。
Linux的开发采用的是集市模型(bazaar,与cathedral--教堂模型--对应),为了确保这些无序的开发过程能够有序地进行,Linux采用了双树系统。
一个树是稳定树(stable tree),另一个树是非稳定树(unstable tree)或者开发树(d evelopment tree)。
一些新特性、实验性改进等都将首先在开发树中进行。
如果在开发树中所做的改进也可以应用于稳定树,那么在开发树中经过测试以后,在稳定树中将进行相同的改进。
Linux环境编程:从应⽤到内核Linux环境编程:从应⽤到内核0 基础知识0.1 ⼀个Linux程序的诞⽣记0.2 程序的构成0.3 程序是如何“跑”的0.4 背景概念介绍0.4.1 系统调⽤0.4.2 C库函数0.4.3 线程安全0.4.4 原⼦性0.4.5 可重⼊函数0.4.6 阻塞与⾮阻塞0.4.7 同步与⾮同步1 ⽂件I/O1.1 Linux中的⽂件1.1.1 ⽂件、⽂件描述符和⽂件表1.1.2 内核⽂件表的实现1.2 打开⽂件1.2.1 open介绍1.2.2 更多选项1.2.3 open源码跟踪1.2.4 如何选择⽂件描述符1.2.5 ⽂件描述符fd与⽂件管理结构file1.3 creat简介1.4 关闭⽂件1.4.1 close介绍1.4.2 close源码跟踪1.4.3 ⾃定义files_operations1.4.4 遗忘close造成的问题1.4.5 如何查找⽂件资源泄漏1.5 ⽂件偏移1.5.1 lseek简介1.5.2 ⼩⼼lseek的返回值1.5.3 lseek源码分析1.6 读取⽂件1.6.1 read源码跟踪1.6.2 部分读取1.7 写⼊⽂件1.7.1 write源码跟踪1.7.2 追加写的实现1.8 ⽂件的原⼦读写1.9 ⽂件描述符的复制1.10 ⽂件数据的同步1.11 ⽂件的元数据1.11.1 获取⽂件的元数据1.11.2 内核如何维护⽂件的元数据1.11.3 权限位解析1.12 ⽂件截断1.12.1 truncate与ftruncate的简单介绍1.12.2 ⽂件截断的内核实现1.12.3 为什么需要⽂件截断2 标准I/O库2.1 stdin、stdout和stderr 2.2 I/O缓存引出的趣题2.3 fopen和open标志位对⽐2.4 fdopen与fileno2.5 同时读写的痛苦2.6 ferror的返回值2.7 clearerr的⽤途2.8 ⼩⼼fgetc和getc2.9 注意fread和fwrite的返回值2.10 创建临时⽂件3 进程环境3.1 main是C程序的开始吗3.2 “活雷锋”exit3.3 atexit介绍3.3.1 使⽤atexit3.3.2 atexit的局限性3.3.3 atexit的实现机制3.4 ⼩⼼使⽤环境变量3.5 使⽤动态库3.5.1 动态库与静态库3.5.2 编译⽣成和使⽤动态库3.5.3 程序的“平滑⽆缝”升级3.6 避免内存问题3.6.1 尴尬的realloc3.6.2 如何防⽌内存越界3.6.3 如何定位内存问题3.7 “长跳转”longjmp3.7.1 setjmp与longjmp的使⽤3.7.2 “长跳转”的实现机制3.7.3 “长跳转”的陷阱4 进程控制:进程的⼀⽣4.1 进程ID4.2 进程的层次4.2.1 进程组4.2.2 会话4.3 进程的创建之fork()4.3.1 fork之后⽗⼦进程的内存关系4.3.2 fork之后⽗⼦进程与⽂件的关系4.3.3 ⽂件描述符复制的内核实现4.4 进程的创建之vfork()4.5 daemon进程的创建4.6 进程的终⽌4.6.1 _exit函数4.6.2 exit函数4.6.3 return退出4.7 等待⼦进程4.7.1 僵⼫进程4.7.2 等待⼦进程之wait()4.7.3 等待⼦进程之waitpid()4.7.4 等待⼦进程之等待状态值4.7.5 等待⼦进程之waitid()4.7.6 进程退出和等待的内核实现4.8 exec家族4.8.1 execve函数4.8.2 exec家族4.8.3 execve系统调⽤的内核实现4.8.4 exec与信号4.8.5 执⾏exec之后进程继承的属性4.9 system函数4.9.1 system函数接⼝4.9.2 system函数与信号4.10 总结5 进程控制:状态、调度和优先级5.1 进程的状态5.1.1 进程状态概述5.1.2 观察进程状态5.2 进程调度概述5.3 普通进程的优先级5.4 完全公平调度的实现5.4.1 时间⽚和虚拟运⾏时间5.4.2 周期性调度任务5.4.3 新进程的加⼊5.4.4 睡眠进程醒来5.4.5 唤醒抢占5.5 普通进程的组调度5.6 实时进程5.6.1 实时调度策略和优先级5.6.2 实时调度相关API5.6.3 限制实时进程运⾏时间5.7 CPU的亲和⼒6 信号6.1 信号的完整⽣命周期6.2 信号的产⽣6.2.1 硬件异常6.2.2 终端相关的信号6.2.3 软件事件相关的信号6.3 信号的默认处理函数6.4 信号的分类6.5 传统信号的特点6.5.1 信号的ONESHOT特性6.5.2 信号执⾏时屏蔽⾃⾝的特性6.5.3 信号中断系统调⽤的重启特性6.6 信号的可靠性6.6.1 信号的可靠性实验6.6.2 信号可靠性差异的根源6.7 信号的安装6.8 信号的发送6.8.1 kill、tkill和tgkill6.8.2 raise函数6.8.3 sigqueue函数6.9 信号与线程的关系6.9.1 线程之间共享信号处理函数6.9.2 线程有独⽴的阻塞信号掩码6.9.3 私有挂起信号和共享挂起信号6.9.4 致命信号下,进程组全体退出6.10 等待信号6.10.1 pause函数6.10.2 sigsuspend函数6.10.3 sigwait函数和sigwaitinfo函数6.11 通过⽂件描述符来获取信号6.12 信号递送的顺序6.13 异步信号安全6.14 总结7 理解Linux线程(1)7.1 线程与进程7.2 进程ID和线程ID7.3 pthread库接⼝介绍7.4 线程的创建和标识7.4.1 pthread_create函数7.4.2 线程ID及进程地址空间布局7.4.3 线程创建的默认属性7.5 线程的退出7.6 线程的连接与分离7.6.1 线程的连接7.6.2 为什么要连接退出的线程7.6.3 线程的分离7.7 互斥量7.7.1 为什么需要互斥量7.7.2 互斥量的接⼝7.7.3 临界区的⼤⼩7.7.4 互斥量的性能7.7.5 互斥锁的公平性7.7.6 互斥锁的类型7.7.7 死锁和活锁7.8 读写锁7.8.1 读写锁的接⼝7.8.2 读写锁的竞争策略7.8.3 读写锁总结7.9 性能杀⼿:伪共享7.10 条件等待7.10.1 条件变量的创建和销毁7.10.2 条件变量的使⽤8 理解Linux线程(2)8.1 线程取消8.1.1 函数取消接⼝8.1.2 线程清理函数8.2 线程局部存储8.2.1 使⽤NPTL库函数实现线程局部存储8.2.2 使⽤__thread关键字实现线程局部存储8.3 线程与信号8.3.1 设置线程的信号掩码8.3.2 向线程发送信号8.3.3 多线程程序对信号的处理9 进程间通信:管道9.1 管道9.1.1 管道概述9.1.2 管道接⼝9.1.3 关闭未使⽤的管道⽂件描述符9.1.4 管道对应的内存区⼤⼩9.1.5 shell管道的实现9.1.6 与shell命令进⾏通信(popen)9.2 命名管道FIFO9.2.1 创建FIFO⽂件9.2.2 打开FIFO⽂件9.3 读写管道⽂件9.4 使⽤管道通信的⽰例10 进程间通信:System V IPC 10.1 System V IPC概述10.1.1 标识符与IPC Key10.1.2 IPC的公共数据结构10.2 System V消息队列10.2.1 创建或打开⼀个消息队列10.2.2 发送消息10.2.3 接收消息10.2.4 控制消息队列10.3 System V信号量10.3.1 信号量概述10.3.2 创建或打开信号量10.3.3 操作信号量10.3.4 信号量撤销值10.3.5 控制信号量10.4 System V共享内存10.4.1 共享内存概述10.4.2 创建或打开共享内存10.4.3 使⽤共享内存10.4.4 分离共享内存10.4.5 控制共享内存11 进程间通信:POSIX IPC 11.1 POSIX IPC概述11.1.1 IPC对象的名字11.1.2 创建或打开IPC对象11.1.3 关闭和删除IPC对象11.1.4 其他11.2 POSIX消息队列11.2.1 消息队列的创建、打开、关闭及删除11.2.2 消息队列的属性11.2.3 消息的发送和接收11.2.4 消息的通知11.2.5 I/O多路复⽤监控消息队列11.3 POSIX信号量11.3.1 创建、打开、关闭和删除有名信号量11.3.2 信号量的使⽤11.3.3 ⽆名信号量的创建和销毁11.3.4 信号量与futex11.4.1 内存映射概述11.4.2 内存映射的相关接⼝11.4.3 共享⽂件映射11.4.4 私有⽂件映射11.4.5 共享匿名映射11.4.6 私有匿名映射11.5 POSIX共享内存11.5.1 共享内存的创建、使⽤和删除11.5.2 共享内存与tmpfs12 ⽹络通信:连接的建⽴12.1 socket⽂件描述符12.2 绑定IP地址12.2.1 bind的使⽤12.2.2 bind的源码分析12.3 客户端连接过程12.3.1 connect的使⽤12.3.2 connect的源码分析12.4 服务器端连接过程12.4.1 listen的使⽤12.4.2 listen的源码分析12.4.3 accept的使⽤12.4.4 accept的源码分析12.5 TCP三次握⼿的实现分析12.5.1 SYN包的发送12.5.2 接收SYN包,发送SYN+ACK包12.5.3 接收SYN+ACK数据包12.5.4 接收ACK数据包,完成三次握⼿13 ⽹络通信:数据报⽂的发送13.1 发送相关接⼝13.2 数据包从⽤户空间到内核空间的流程13.3 UDP数据包的发送流程13.4 TCP数据包的发送流程13.5 IP数据包的发送流程13.5.1 ip_send_skb源码分析13.5.2 ip_queue_xmit源码分析13.6 底层模块数据包的发送流程14 ⽹络通信:数据报⽂的接收14.1 系统调⽤接⼝14.2 数据包从内核空间到⽤户空间的流程14.3 UDP数据包的接收流程14.4 TCP数据包的接收流程14.5 TCP套接字的三个接收队列14.6 从⽹卡到套接字14.6.1 从硬中断到软中断14.6.2 软中断处理14.6.3 传递给协议栈流程14.6.4 IP协议处理流程14.6.5 ⼤师的错误?原始套接字的接收14.6.6 注册传输层协议14.6.7 确定UDP套接字14.6.8 确定TCP套接字15 编写安全⽆错代码15.1 不要⽤memcmp⽐较结构体15.2 有符号数和⽆符号数的移位区别15.3 数组和指针15.4 再论数组⾸地址15.5 “神奇”的整数类型转换15.6 ⼩⼼volatile的原⼦性误解15.7 有趣的问题:“x==x”何时为假?15.8 ⼩⼼浮点陷阱15.8.1 浮点数的精度限制15.8.2 两个特殊的浮点值15.9 Intel移位指令陷阱思维导图思维导图在线编辑链接:。
Getting Started V2.8.1, 2020-11-29 The LinuxCNC Team该手册正在编写中。
如果您能够在编写, 编辑或者图片准备上为我们提供帮助, 联系文档编写团队任何成员,或加入我们团队。
发送电子邮件至***************************.net版权所有©2000-2020 授予复制,分发和/或修改本文档的权限 根据GNU Free Documentation License Version 1.1的条款 或自由软件基金会发布的任何更高版本; 没有不变的部分,没有前封面文字,也没有后封面文字。
许可证的副本包含在标题为“GNU Free Documentation License”中。
LINUX®是Linus Torvalds在美国和其他国家/地区的注册商标。
注册商标Linux®是根据来自Linux商标协会(LMI)分许可证使用, LMI是Linus Torvalds的独家许可持有人,全球范围内商标的所有者。
LinuxC NC项目不属于Debian®。
Debian是公益软件公司(Software in the Public Interest,Inc.)拥有的注册商标,Linux C N C项目不属于UBUNTU®。
B NT是科能软件有限公司(C anonical Limited)拥有的注册商标。
关于LinuxCNC软件•LinuxCNC(增强型机器控制器)是一个软件系统,用于机床(例如铣床和车床),机器人(例如puma 和scara)以及其他最多9轴的计算机控制器。
•LinuxCNC是开源代码的免费软件。
当前版本的LinuxCNC完全根据GNU通用公共许可证和次要GNU通用公共许可证(GPL和LGPL)获得许可•LinuxCNC提供:◦图形用户界面(实际上是几个界面可供选择)◦G代码的解释器(RS-274机床编程语言)◦具有超前的实时运动调度系统◦底层机器电子设备(例如传感器和电机驱动器)的操作◦易于使用的"面包板"层,可为您的机器快速创建独特的配置◦可通过梯形图编程的软件PLC◦使用Live-CD轻松安装•不提供工程图(CAD-计算机辅助设计)或从工程图生成G代码(CAM-计算机自动化制造)的功能。
第1章Hello, World如果第一个程序员是一个山顶洞人,它在山洞壁(第一台计算机)上凿出的第一个程序应该是用羚羊图案构成的一个字符串“Hello, Wo r l d”。
罗马的编程教科书也应该是以程序“S a l u t, M u n d i”开始的。
我不知道如果打破这个传统会带来什么后果,至少我还没有勇气去做第一个吃螃蟹的人。
内核模块至少必须有两个函数:i n i t_m o d u l e和c l e a n u p_m o d u l e。
第一个函数是在把模块插入内核时调用的;第二个函数则在删除该模块时调用。
一般来说,i n i t_m o d u l e可以为内核的某些东西注册一个处理程序,或者也可以用自身的代码来取代某个内核函数(通常是先干点别的什么事,然后再调用原来的函数)。
函数c l e a n u p_m o d u l e的任务是清除掉i n i t_m o d u l e所做的一切,这样,这个模块就可以安全地卸载了。
1.1 内核模块的Makefiles 文件内核模块并不是一个独立的可执行文件,而是一个对象文件,在运行时内核模块被链接到内核中。
因此,应该使用- c 命令参数来编译它们。
还有一点需要注意,在编译所有内核模块时,都将需要定义好某些特定的符号。
• _ _KERNEL_ _—这个符号告诉头文件:这个程序代码将在内核模式下运行,而不要作为用户进程的一部分来执行。
• MODULE —这个符号告诉头文件向内核模块提供正确的定义。
• L I N U X —从技术的角度讲,这个符号不是必需的。
然而,如果程序员想要编写一个重要的内核模块,而且这个内核模块需要在多个操作系统上编译,在这种情况下,程序员将会很高兴自己定义了L I N U X 这个符号。
这样一来,在那些依赖于操作系统的部分,这个符号就可以提供条件编译了。
还有其它的一些符号,是否包含它们要取决于在编译内核时使用了哪些命令参数。
如果用户不太清楚内核是怎样编译的,可以查看文件/ u s r /i n c l u d e /l i n u x /c o n f i g .h 。
• _ _SMP_ _—对称多处理。
如果编译内核的目的是为了支持对称多处理,在编译时就需要定义这个符号(即使内核只是在一个C P U 上运行也需要定义它)。
当然,如果用户使用对称多处理,那么还需要完成其它一些任务(参见第1 2章)。
• C O N F I G _M O D V E R S I O N S —如果C O N F I G _M O D V E R S I O N S 可用,那么在编译内核模块时就需要定义它,并且包含头文件/ u s r /i n c l u d e /l i n u x /m o d v e r s i o n s .h 。
还可以用代码自身来完成这个任务。
完成了以上这些任务以后,剩下唯一要做的事就是切换到根用户下(你不是以r o o t 身份编译内核模块的吧?别玩什么惊险动作哟!),然后根据自己的需要插入或删除h e l l o 模块。
在执行完i n s m o d 命令以后,可以看到新的内核模块在/ p r o c /m o d u l e s 中。
顺便提一下,M a k e f i l e 建议用户不要从X 执行i n s m o d 命令的原因在于,当内核有个消息需要使用p r i n t k 命令打印出来时,内核会把该消息发送给控制台。
当用户没有使用X 时,该消息146第二部分Linux 内核模块编程指南将发送到用户正在使用的虚拟终端(用户可以用A l t-F<n>来选择当前终端),然后用户就可以看到这个消息了。
而另一方面,当用户使用X时,存在两种可能性。
一种情况是用户用命令xterm -C打开了一个控制台,这时输出将被发送到那个控制台;另一种情况是用户没有打开控制台,这时输出将送往虚拟终端7—被X所“覆盖”的一个虚拟终端。
当用户的内核不太稳定时,没有使用X的用户更有可能取得调试信息。
如果没有使用X,p r i n t k将直接从内核把调试消息发送到控制台。
而另一方面,在X中p r i n t k的消息将被送给一个用户模式的进程(xterm -C)。
当那个进程获得C P U时间时,它将把该消息传送给X服务器进程。
然后,当X服务器获得C P U时间时,它将显示该消息—但是一个不稳定的内核通常意味着系统将要崩溃或者重新启动,所以用户不希望推迟错误信息显示的时间,因为该信息可能会向用户解释什么地方出了问题,如果显示的时刻晚于系统崩溃或重启的时刻,用户将会错过这个重要的信息。
1.2 多重文件内核模块有时候在多个源文件间划分内核模块是很有意义的。
这时用户需要完成下面三件任务:1) 除了一个源文件以外,在其它所有源文件中加入一行#define _ _ NO_VERSION_ _。
这点很重要,因为m o d u l e.h中通常会包含有k e r n e l_v e r s i o n的定义( k e r n e l_v e r s i o n是一个全局变量,它表明该模块是为哪个内核版本所编译的)。
如果用户需要v e r s i o n.h文件,那么用户必须自己把它包含在源文件中,因为在定义了_ _NO_VERSION_ _的情况下,m o d u l e.h是不会为用户完成这个任务的。
2) 像平常一样编译所有的源文件。
3) 把所有的对象文件组合进一个文件中。
在x 86下,可以使用命令:ld -m elf_i386 -r -o〈模块名称〉.o (第一个源文件).o (第二个源文件) .o来完成这个任务。
下面是这种内核模块的一个例子。
148第二部分Linux 内核模块编程指南第2章字符设备文件我们现在就可以吹牛说自己是内核程序员了。
虽然我们所写的内核模块还什么也干不了,但我们仍然为自己感到骄傲,简直可以称得上趾高气扬。
但是,有时候在某种程度上我们也会感到缺少点什么,简单的模块并不是太有趣。
内核模块主要通过两种方法与进程打交道。
一种方法是通过设备文件(例如在目录/ d e v中的文件),另一种方法是使用p r o c文件系统。
既然编写内核模块的主要原因之一就是支持某些类型的硬件设备,那么就让我们从设备文件开始吧。
设备文件最初的用途是使进程与内核中的设备驱动程序通信,并且通过设备驱动程序再与物理设备(调制解调器、终端等等)通信。
下面我们要讲述实现这一任务的方法。
每个设备驱动程序都被赋予一个主编号,主要用于负责某几种类型的硬件。
可以在/ p r o c/d e v i c e s中找到驱动程序以及它们对应的主编号的列表。
由设备驱动程序管理的每个物理设备都被赋予一个从编号。
这些设备中的每一个,不管是否真正安装在计算机系统上,都将对应一个特殊的文件,该文件称为设备文件,所有的设备文件都包含在目录/ d e v中。
例如,如果执行命令ls -l /dev/hd[ab]*,用户将可以看到与一个计算机相连接的所有的I D E硬件分区。
注意,所有的这些硬盘分区都使用同一个主编号:3,但是从编号却各不相同。
需要强调的是,这里假设用户使用的是P C体系结构。
我并不知道在其它体系结构上运行的L i n u x的设备是怎么样的。
在安装了系统以后,所有的设备文件都由命令m k n o d创建出来。
从技术的角度上讲,并没有什么特别的原因一定要把这些设备文件放在目录/ d e v中,这只不过是一个有用的传统习惯而已。
如果读者创建设备文件的目的只不过是为了试试看,就像本章的练习一样,那么把该设备文件放置在编译内核模块的目录中可能会更有意义一些。
设备一般分为两种类型:字符设备和块设备。
它们的区别在于块设备具有一个请求缓冲区,所以块设备可以选择按照何种顺序来响应这些请求。
这对于存储设备来说是很重要的。
在存储设备中,读或写相邻的扇区速度要快一些,而读写相互之间离得较远的扇区则要慢得多。
另一个区别在于块设备只能以成块的形式接收输入和返回输出(块的大小根据设备类型的变化而有所不同),而字符设备则可以随心所欲地使用任意数目的字节。
当前大多数设备都是字符设备,因为它们既不需要某种形式的缓冲,也不需要按照固定的块大小来进行操作。
如果想知道某个设备文件对应的是块设备还是字符设备,用户可以执行命令ls -l,查看一下该命令的输出中的第一个字符,如果第一个字符是“b”,则对应的是块设备;如果是“c”,则对应的是字符设备。
模块分为两个独立的部分:模块部分和设备驱动程序部分。
前者用于注册设备。
函数i n i t_m o d u l e调用m o d u l e_r e g i s t e r_c h r d e v,把该设备驱动程序加入到内核的字符设备驱动程序表中,它还会返回供驱动程序所使用的主编号。
函数c l e a n u p_m o d u l e则取消该设备的注册。
注册某设备和取消它的注册是这两个函数最基本的功能。
内核中的东西并不是按照它们自己的意愿主动开始运行的,就像进程一样,而是由进程通过系统调用来调用,或者由硬件设备通过中断来调用,或者由内核的其它部分调用(只需调用特定的函数),它们才会执行。
因此,如果用户往内核中加入了代码,就必须把它作为某种特定类型事件的处理程序进行注册;而在删除这些代码时,用户必须取消它的注册。
设备驱动程序一般是由四个d e v i c e_<a c t i o n>函数所组成的,如果用户需要处理具有对应主编号的设备文件,就可以调用这四个函数。
通过f i l e_o p e r a t i o n s结构F o p s内核可以知道调用哪些函数。
因为该结构的值是在注册设备时给定的,它包含了指向这四个函数的指针。
在这里我们还需要记住的一点是:无论如何不能乱删内核模块。
原因在于如果设备文件是由进程打开的,而我们删去了该内核模块,那么使用该文件就将导致对正确的函数(读/写)原来所处的存储位置的调用。
如果我们走运,那里没有装入什么其它的代码,那我们至多得到一些难看的错误信息,而如果我们不走运,在原来的同一位置已经装入了另一个内核模块,这就意味着跳转到了内核中另一个函数的中间,这样做的后果是不堪设想的,起码不会是令人愉快的。
一般来说,如果用户不愿意让某件事发生,可以让执行这件事的函数返回一个错误代码(一个负数)。
而对c l e a n u p_m o d u l e来说这是不可能的,因为它是一个v o i d函数。
一旦调用了c l e a n u p_m o d u l e,这个模块就死了。
然而,还有一个计数器记录了有多少个其它的内核模块正在使用该内核模块,这个计数器称为引用计数器(就是位于文件/ p r o c/m o d u l e s信息行中的最后那个数值)。