linux内核编程.rtf
Linux 内核编程 目目 录录1HELLO,WORLD.EXHELLO.C.11 内核模块的编译文件.1.2 多文件内核模块.2字符设备文件字符设备文件.21 多内核版本源文件.3/PROC 文件系统文件系统.4使用使用/PROC 进行输入进行输入.5和设备文件对话(写和和设备文件对话(写和 IOCTLS).6启动参数启动参数.7系统调用系统调用.8阻塞进程阻塞进程.9替换替换 PRINTKS.10调度任务调度任务.11中断处理程序中断处理程序.11.1 INTEL 结构上的键盘.12对称多处理对称多处理.常见的错误常见的错误.2.0 和和 2.2 版本的区别版本的区别.除此以外除此以外.其他其他.GOODS AND SERVICES.GNU GENERAL PUBLIC LICENSE.注注.11Hello,world当第一个穴居的原始人程序员在墙上凿出第一个“洞穴计算机”的程序时,那是一个打印出用羚羊角上的图案表示的“Hello world”的程序。罗马编程教科书上是以“Salut,Mundi”的程序开始的。我不知道如果人们打破这个传统后会有什么后果,但我认为还是不要去发现这个后果比较安全。一个内核模块至少包括两个函数:init_module,在这个模块插入内核时调用;cleanup_module,在模块被移出时调用。典型情况下,init_module 为内核中的某些东西注册一个句柄,或者把内核中的程序提换成它自己的代码(通常是进行一些工作以后再调用原来工作的代码)。Clean_module 模块要求撤销 init_module 进行的所有处理工作,使得模块可以被安全的卸载。Exhello.c/*hello.c *Copyright(C)1998 by Ori Pomerantz*Hello,world-the kernel module version.*/*The necessary header files*/*Standard in kernel modules*/#include /*Were doing kernel work*/#include /*Specifically,a module*/*Deal with CONFIG_MODVERSIONS*/#if CONFIG_MODVERSIONS=1#define MODVERSIONS#include#endif /*Initialize the module*/int init_module()printk(Hello,world-this is the kernel speakingn);2 /*If we return a non zero value,it means that *init_module failed and the kernel module *cant be loaded*/return 0;/*Cleanup-undid whatever init_module did*/void cleanup_module()printk(Short is the life of a kernel modulen);11 内核模块的编译文件内核模块的编译文件 一个内核模块不是一个可以独立执行的文件,而是需要在运行时刻连接入内核的目标文件。所以,它们需要用-c 选项进行编译。而且,所有的内核模块都必须包含特定的标志:_KERNEL_这个标志告诉头文件此代码将在内核模块中运行,而不是作为用户进程。MODULE这个标志告诉头文件要给出适当的内核模块的定义。LINUX从技术上讲,这个标志不是必要的。但是,如果你希望写一个比较正规的内核模块,在多个操作系统上编译,这个标志将会使你感到方便。它可以允许你在独立于操作系统的部分进行常规的编译。还有其它的一些可被选择包含标志,取决于编译模块是的选项。如果你不能明确内核怎样被编译,可以在 in/usr/include/linux/config.h 中查到。_SMP_对称多线程。在内核被编译成支持对称多线程(尽管在一台处理机上运行)是必须定义。如果是这样,还需要做一些别的事情(参见第 12 章)。CONFIG_MODVERSIONS如果 CONFIG_MODVERSIONS 被激活,你需要在编译是定义它并且包含文件/usr/include/linux/modversions.h。这可以有代码自动完成。ex MakefileMakefile#Makefile for a basic kernel moduleCC=gccMODCFLAGS:=-Wall-DMODULE-D_KERNEL_-DLINUXhello.o:hello.c/usr/include/linux/version.h$(CC)$(MODCFLAGS)-c hello.cecho insmod hello.o to turn it onecho rmmod hello to turn if off3echoecho X and kernel programming do not mix.echo Do the insmod and rmmod from outside 所以,并不是剩下的事情就是 root(你没有把它编译成 root,而是在边缘(注 1.1)。对吗?),然后就在你的核心内容里插入或移出 hello。当你这样做的时候,要注意到你的新模块在/proc/modules 里。而且,编译文件不推荐从 X 下插入的原因是内核有一条需要用 printk 打印的消息,它把它送给了控制台。如果你不使用 X,它就送到了你使用的虚拟终端(你用 Alt-F选择的哪个)并且你可以看到。相反的,如果你使用了 X,就有两种可能性。如果用 xterm C 打开了一个控制台,输出将被送到哪里。如果没有,输出将被送到虚拟终端 7被 X“覆盖”的那个。如果你的内核变得不稳定,你可以在没有 X 的情况下得到调试消息。在 X 外,printk可以直接从内核中输出到控制台。而如果在 X 里,printk 输出到一个用户态的进程(xterm C)。当进程接收到 CPU 时间,它会将其送到 X 服务器进程。然后,当 X 服务器进程接收到 CPU 时间,它将会显示,但是一个不稳定的内核意味着系统将会崩溃或重起,所以你不希望显示错误的消息,然后可能被解释给你什么发生了错误,但是超出了正确的时间。1.2 多文件内核模块多文件内核模块有些时候在几个源文件之间分出一个内核模块是很有意义的。在这种情况下,你需要做下面的事情:1.在除了一个以外的所有源文件中,增加一行#define _NO_VERSION_。这是很重要的,因为 module.h 一般包括 kernel_version 的定义,这是一个全局变量,包含模块编译的内核版本。如果你需要 version.h,你需要把自己把它包含进去,因为如果有_NO_VERSION_的话 module.h 不会自动包含。2.象通常一样编译源文件。3.把所有目标文件联编成一个。在 X86 下,用 ld m elf_i386 r o.o 这里给出一个这样的内核模块的例子。ex start.c /*start.c *Copyright(C)1999 by Ori Pomerantz*Hello,world-the kernel module version.*This file includes just the start routine*/*The necessary header files*/*Standard in kernel modules*/#include /*Were doing kernel work*/#include /*Specifically,a module*/4/*Deal with CONFIG_MODVERSIONS*/#if CONFIG_MODVERSIONS=1#define MODVERSIONS#include#endif /*Initialize the module*/int init_module()printk(Hello,world-this is the kernel speakingn);/*If we return a non zero value,it means that *init_module failed and the kernel module *cant be loaded*/return 0;ex stop.c /*stop.c *Copyright(C)1999 by Ori Pomerantz*Hello,world-the kernel module version.This *file includes just the stop routine.*/*The necessary header files*/*Standard in kernel modules*/#include /*Were doing kernel work*/#define _NO_VERSION_ /*This isnt the file *of the kernel module*/#include /*Specifically,a module*/#include /*Not included by *module.h because *of the _NO_VERSION_*/5/*Deal with CONFIG_MODVERSIONS*/#if CONFIG_MODVERSIONS=1#define MODVERSIONS#include#endif /*Cleanup-undid whatever init_module did*/void cleanup_module()printk(Short is the life of a kernel modulen);ex Makefile#Makefile for a multifile kernel moduleCC=gccMODCFLAGS:=-Wall-DMODULE-D_KERNEL_-DLINUXhello.o:start.o stop.old-m elf_i386-r-o hello.o start.o stop.ostart.o:start.c/usr/include/linux/version.h$(CC)$(MODCFLAGS)-c start.cstop.o:stop.c/usr/include/linux/version.h$(CC)$(MODCFLAGS)-c stop.c62字符设备文件字符设备文件那么,现在我们是原始级的内核程序员,我们知道如何写不做任何事情的内核模块。我们为自己而骄傲并且高昂起头来。但是不知何故我们感觉到缺了什么东西。患有精神紧张症的模块不是那么有意义。内核模块同进程对话有两种主要途径。一种是通过设备文件(比如/dev 目录中的文件),另一种是使用 proc 文件系统。我们把一些东西写入内核的一个主要原因就是支持一些硬件设备,所以我们从设备文件开始。设备文件的最初目的是允许进程同内核中的设备驱动通信,并且通过它们和物理设备通信(modem,终端,等等)。这种方法的实现如下:每个设备驱动都对应着一定类型的硬件设备,并且被赋予一个主码。设备驱动的列表和它们的主码可以在 in/proc/devices 中找到。每个设备驱动管理下的物理设备也被赋予一个从码。无论这些设备是否真的安装,在/dev 目录中都将有一个文件,称作设备文件,对应着每一个设备。例如,如果你进行 ls l/dev/hdab*操作,你将看见可能联结到某台机器上的所有的 IDE硬盘分区。注意它们都使用了同一个主码,3,但是从码却互不相同。(声明:这是在 PC 结构上的情况,我不知道在其他结构上运行的 linux 是否如此。)在系统安装时,所有设备文件在 mknod 命令下被创建。它们必须创建在/dev 目录下没有技术上的原因,只是一种使用上的便利。如果是为测试目的而创建的设备文件,比如我们这里的练习,可能放在你编译内核模块的的目录下更加合适。设备可以被分成两类:字符设备和块设备。它们的区别是块设备有一个用于请求的缓冲区,所以它们可以选择用什么样的顺序来响应它们。这对于存储设备是非常重要的,读取相邻的扇区比互相远离的分区速度会快得多。另一个区别是块设备只能按块(块大小对应不同设备而变化)接受输入和返回输出,而字符设备却按照它们能接受的最少字节块来接受输入。大部分设备是字符设备,因为它们不需要这种类型的缓冲。你可以通过观看 ls-l 命令的输出中的第一个字符而知道一个设备文件是块设备还是字符设备。如果是 b 就是块设备,如果是 c 就是字符设备。这个模块可以被分成两部分:模块部分和设备及设备驱动部分。Init_module 函数调用module_register_chrdev 在内核得块设备表里增加设备驱动。同时返回该驱动所使用的主码。Cleanup_module 函数撤销设备的注册。这些操作(注册和注销)是这两个函数的主要功能。内核中的函数不是象进程一样自发运行的,而是通过系统调用,或硬件中断或者内核中的其它部分(只要是调用具体的函数)被进程调用的。所以,当你向内和中增加代码时,你应该把它注册为具体某种事件的句柄,而当你把它删除的时候,你需要注销这个句柄。设备驱动完全由四个设备_action函数构成,它们在希望通过有主码的设备文件实现一些操作时被调用。内核调用它们的途径是通过 file_operation 结构 Fops。此结构在设备被注册是创建,它包含指向这四个函数的指针。另一点我们需要记住的是,我们不能允许管理员随心所欲的删除内核模块。这是因为如果设备文件是被进程打开的,那么我们删除内核模块的时候,要使用这些文件就会导致访问正常的函数(读/写)所在的内存位置。如果幸运,那里不会有其他代码被装载,我们将得到一个恶性的错误信息。如果不行,另一个内核模块会被装载到同一个位置,这将意味着7会跳入内核中另一个程序的中间,结果将是不可预料的恶劣。通常你不希望一个函数做什么事情的时候,会从那个函数返回一个错误码(一个负数)。但这在 cleanup_module 中是不可能的,因为它是一个 void 型的函数。一旦cleanup_module 被调用,这个模块就死掉了。然而有一个计数器记录着有多少个内核模块在使用这个模块,这个计数器称为索引计数器(/proc/modules 中没行的最后一个数字)。如果这个数字不是 0,删除就会失败。模块的索引计数器包含在变量 mod_use_count_中。有定义好的处理这个变量的宏(MOD_INC_USE_COUNT 和 MOD_DEC_USE_COUNT),所以我们一般使用宏而不是直接使用变量 mod_use_count_,这样在以后实现变化的时候会带来安全性。ex chardev.c /*chardev.c *Copyright(C)1998-1999 by Ori Pomerantz*Create a character device(read only)*/*The necessary header files*/*Standard in kernel modules*/#include /*Were doing kernel work*/#include /*Specifically,a module*/*Deal with CONFIG_MODVERSIONS*/#if CONFIG_MODVERSIONS=1#define MODVERSIONS#include#endif /*For character devices*/#include /*The character device *definitions are here*/#include /*A wrapper which does *next to nothing at *at present,but may *help for compatibility *with future versions *of Linux*/*In 2.2.3/usr/include/linux/version.h includes *a macro for this,but 2.0.35 doesnt-so I add *it here if necessary.*/8#ifndef KERNEL_VERSION#define KERNEL_VERSION(a,b,c)(a)*65536+(b)*256+(c)#endif/*Conditional compilation.LINUX_VERSION_CODE is *the code(as per KERNEL_VERSION)of this version.*/#if LINUX_VERSION_CODE KERNEL_VERSION(2,2,0)#include /*for put_user*/#endif#define SUCCESS 0/*Device Declarations*/*The name for our device,as it will appear *in/proc/devices*/#define DEVICE_NAME char_dev/*The maximum length of the message from the device*/#define BUF_LEN 80/*Is the device open right now?Used to prevent *concurent access into the same device*/static int Device_Open=0;/*The message the device will give when asked*/static char MessageBUF_LEN;/*How far did the process reading the message *get?Useful if the message is larger than the size *of the buffer we get to fill in device_read.*/static char*Message_Ptr;/*This function is called whenever a process *attempts to open the device file*/static int device_open(struct inode*inode,struct file*file)9 static int counter=0;#ifdef DEBUG printk(device_open(%p,%p)n,inode,file);#endif /*This is how you get the minor device number in *case you have more than one physical device using *the driver.*/printk(Device:%d.%dn,inode-i_rdev 8,inode-i_rdev&0 xFF);/*We dont want to talk to two processes at the *same time*/if(Device_Open)return-EBUSY;/*If this was a process,we would have had to*be more careful here.*In the case of processes,the danger would be*that one process might have check Device_Open*and then be replaced by the schedualer by another*process which runs this function.Then,when*the first process was back on the CPU,it would assume*the device is still not open.*However,Linux guarantees that a process wont*be replaced while it is running in kernel context.*In the case of SMP,one CPU might increment*Device_Open while another CPU is here,right after the check.*However,in version 2.0 of the kernel this is not a problem*because theres a lock to guarantee only one CPU will*be kernel module at the same time.*This is bad in terms of performance,so version 2.2 changed it.*Unfortunately,I dont have access to an SMP box*to check how it works with SMP.*/Device_Open+;/*Initialize the message.*/sprintf(Message,If I told you once,I told you%d times-%s,10 counter+,Hello,worldn);/*The only reason were allowed to do this sprintf *is because the maximum length of the message *(assuming 32 bit integers-up to 10 digits *with the minus sign)is less than BUF_LEN,which *is 80.BE CAREFUL NOT TO OVERFLOW BUFFERS,*ESPECIALLY IN THE KERNEL!*/Message_Ptr=Message;/*Make sure that the module isnt removed while *the file is open by incrementing the usage count *(the number of opened references to the module,if *its not zero rmmod will fail)*/MOD_INC_USE_COUNT;return SUCCESS;/*This function is called when a process closes the *device file.It doesnt have a return value in *version 2.0.x because it cant fail(you must ALWAYS*be able to close a device).In version 2.2.x it is *allowed to fail-but we wont let it.*/#if LINUX_VERSION_CODE=KERNEL_VERSION(2,2,0)static int device_release(struct inode*inode,struct file*file)#else static void device_release(struct inode*inode,struct file*file)#endif#ifdef DEBUG printk(device_release(%p,%p)n,inode,file);#endif /*Were now ready for our next caller*/Device_Open-;11 /*Decrement the usage count,otherwise once you *opened the file youll never get rid of the module.*/MOD_DEC_USE_COUNT;#if LINUX_VERSION_CODE=KERNEL_VERSION(2,2,0)return 0;#endif/*This function is called whenever a process which *have already opened the device file attempts to *read from it.*/#if LINUX_VERSION_CODE=KERNEL_VERSION(2,2,0)static ssize_t device_read(struct file*file,char*buffer,/*The buffer to fill with data*/size_t length,/*The length of the buffer*/loff_t*offset)/*Our offset in the file*/#elsestatic int device_read(struct inode*inode,struct file*file,char*buffer,/*The buffer to fill with *the data*/int length)/*The length of the buffer *(mustnt write beyond that!)*/#endif /*Number of bytes actually written to the buffer*/int bytes_read=0;/*If were at the end of the message,return 0 *(which signifies end of file)*/if(*Message_Ptr=0)return 0;/*Actually put the data into the buffer*/while(length&*Message_Ptr)/*Because the buffer is in the user data segment,*not the kernel data segment,assignment wouldnt *work.Instead,we have to use put_user which 12 *copies data from the kernel data segment to the *user data segment.*/put_user(*(Message_Ptr+),buffer+);length-;bytes_read+;#ifdef DEBUG printk(Read%d bytes,%d leftn,bytes_read,length);#endif /*Read functions are supposed to return the number *of bytes actually inserted into the buffer*/return bytes_read;/*This function is called when somebody tries to write *into our device file-unsupported in this example.*/#if LINUX_VERSION_CODE=KERNEL_VERSION(2,2,0)static ssize_t device_write(struct file*file,const char*buffer,/*The buffer*/size_t length,/*The length of the buffer*/loff_t*offset)/*Our offset in the file*/#elsestatic int device_write(struct inode*inode,struct file*file,const char*buffer,int length)#endif return-EINVAL;/*Module Declarations*/13/*The major device number for the device.This is *global(well,static,which in this context is global*within this file)because it has to be accessible *both for registration and for release.*/static int Major;/*This structure will hold the functions to be *called when a process does something to the device *we created.Since a pointer to this structure is *kept in the devices table,it cant be local to*init_module.NULL is for unimplemented functions.*/struct file_operations Fops=NULL,/*seek*/device_read,device_write,NULL,/*readdir*/NULL,/*select*/NULL,/*ioctl*/NULL,/*mmap*/device_open,#if LINUX_VERSION_CODE=KERNEL_VERSION(2,2,0)NULL,/*flush*/#endif device_release /*a.k.a.close*/;/*Initialize the module-Register the character device*/int init_module()/*Register the character device(atleast try)*/Major=module_register_chrdev(0,DEVICE_NAME,&Fops);/*Negative values signify an error*/if(Major 0)printk(%s device failed with%dn,Sorry,registering the character,Major);return Major;14 printk(%s The major device number is%d.n,Registeration is a success.,Major);printk(If you want to talk to the device driver,n);printk(youll have to create a device file.n);printk(We suggest you use:n);printk(mknod c%d n,Major);printk(You can try different minor numbers%s,and see what happens.n);return 0;/*Cleanup-unregister the appropriate file from/proc*/void cleanup_module()int ret;/*Unregister the device*/ret=module_unregister_chrdev(Major,DEVICE_NAME);/*If theres an error,report it*/if(ret 0)printk(Error in unregister_chrdev:%dn,ret);21 多内核版本源文件多内核版本源文件系统调用是内核出示给进程的主要接口,在不同版本中一般是相同的。可能会增加新的系统,但是旧的系统的行为是不变的。向后兼容是必要的新的内核版本不能打破正常的进程规律。在大多数情况下,设备文件是不变的。然而,内核中的内部接口是可以在不同版本间改变的。Linux 内核的版本分为稳定版(n.m)和发展版(n.m)。发展版包含了所有新奇的思想,包括那些在下一版中被认为是错的,或者被重新实现的。所以,你不能相信在那些版本中这些接口是保持不变的(这就是为什么我在本书中不厌其烦的支持不同接口。这是很大量的工作但是马上就会过时)。但是在稳定版中我们就可以认为接口是相同的,即使在修正版中(数字 m 所指的)。MPG 版本包括了对内核 2.0.x 和 2.2.x 的支持。这两种内核仍有不同之处,所以编译时要取决于内核版本而决定。方法是使用宏 LINUX_VERSION_CODE。在 a.b.c 版中,这个宏的值是 216a+28b+c。如果希望得到具体内核版本号,我们可以使用宏 KERNEL_VERSION。在 2.0.35 版中没有定义这个宏,在需要时我们可以自己定义。153/proc 文件系统文件系统在 Linux 中有一个另外的机制来使内核及内核模块发送信息给进程/proc 文件系统。/proc 文件系统最初是设计使得容易得到进程的信息(从名字可以看出),现在却被任意一块有内容需要报告的内核使用,比如拥有模块列表的/proc/modules 和拥有内存使用统计信息的/proc/meminfo。使用 proc 文件系统的方法很象使用设备驱动你创建一个数据结构,使之包含/proc文件需要的全部信息,包括所有函数的句柄(在我们的例子里只有一个,在试图读取/proc文件时调用)。然后,用 init_module 注册这个结构,用 cleanup_module 注销。我们使用 proc_register_dynamic(注 3.1)的原因是我们不希望决定以后在文件中使用的索引节点数,而是让内核来决定它,为了防止冲突。标准的文件系统是在磁盘上而不是在内存(/proc 的位置在内存),在这种情况下节点数是一个指向文件的索引节点所在磁盘地址的指针。这个索引节点包含了文件的有关信息比如文件的访问权限以及指向磁盘地址的指真或者文件数据的位置。因 为 在 文 件 打 开 或 关 闭 时 我 们 没 有 调 用,所 以 在 模 块 里 无 处 可 放 宏MOD_INC_USE_COUNT 和 MOD_DEC_USE_COUNT,而且如果文件被打开了或者模块被删除了,就没有办法来避免这个结果。下一章我们将会看到一个更困难的处理/proc 的方法,但是也更加灵活,也能够解决这个问题。ex procfs.c /*procfs.c-create a file in/proc *Copyright(C)1998-1999 by Ori Pomerantz*/*The necessary header files*/*Standard in kernel modules*/#include /*Were doing kernel work*/#include /*Specifically,a module*/*Deal with CONFIG_MODVERSIONS*/#if CONFIG_MODVERSIONS=1#define MODVERSIONS#include#endif /*Necessary because we use the proc fs*/#include 16/*In 2.2.3