Linux系统划分为用户空间和内核空间。
用户空间有应用程序和应用程序运行时使用的一些库,内核空间包含七大子系统。现代CPU通常实现了不同的工作模式,以ARM为例,有7种工作模式:用户模式、管理模式、快速中断、外部中断、数据访问中止、未定义指令中止、系统模式。其中USR模式和SVC模式,这两种本身硬件上就定义了自己的访问权限,后者的权限最高,能访问所有硬件资源。Linux系统的软件形式空间的划分要依赖处理器的这种工作模式的划分。由于这种划分用户空间的软件不能随意访问硬件资源、内核空间的地址(代码和数据),如果用户要访问内核空间必须通过系统调用。这种实现最终起到安全的保护。
内核七大子系统
系统调用接口:为用户提供一套标准的系统调用函数来访问Linux内核。
进程管理:用户进程的创建,停止,通信和调度。
内存管理:用于多个进程安全的共享内存区域。
网络协议栈:实现网络数据的传输。
设备驱动:用于控制操作硬件。
虚拟文件系统:隐藏各文件系统的具体细节,为文件提供统一的操作接口。
平台相关:与具体处理器架构相关的实现。
Linux系统调用:
Linux的系统调用大致可以分为:进程控制、文件系统控制、系统控制、内存管理、socket控制、用户管理、进程间通信等几大类。Linux实现系统调用利用了软中断(x86中的 int $0x80 汇编指令以及ARM中的swi 指令)。通常系统调用靠C库支持。
系统调用实现原理,以ARM和open为例
1. 应用程序调用open,首先调用C 库的open实现。
2. C库的open将open对应的系统调用号(__NR_open)填充到寄存器(R7)。
3. open的实现中调用软中断指令(swi),出发一个软中断异常。CPU跳转到异常向量表的软中断入口执行,至此进程由用户空间转向内核空间。
4. 异常向量表有内核在初始化是建立,入口在Linux内核空间的地址为0XFFFF0000。
5. 进程跳转到软中断入口vector_swi后根据之前寄存器中保存的系统调用号,以此为索引在系统调用表中找到对应的内核实现函数sys_open,并执行内核函数。
6. 执行sys_open后转入ret_from_sys_call例程,从系统调用中返回。
系统调用会进入内核执行,但仍处于进程上下文中,因此可以访问进程的许多信息。系统调用是用户空间和内核交互的唯一手段,但是完成交互功能并非需要添加新的系统调用不可。可以使用以下几种方式与内核交互:编写字符驱动程序、使用proc文件系统、使用虚拟文件系统。新的进程可以使用相同的系统调用,必须保证系统调用是重入的。
驱动模块的编译:静态编译、模块化编译
模块的编译通常使用makefile
模块的makefile范例
ifneq($(KERNELRELEASE), ) //变量不等于空则执行下面语句,不过第一次
obj-m :=hello.o //执行makefile,这个变量一般为空。
else //所以执行else的语句。
KDIR :=/lib/modules/2.6.18-53.e15/build //给出内核源代码的路径
all: //M表示内核模块的代码的路径,(此处当前目录)
make –C $(KDIR) M=$(PWD) modules //使用-C指定目录下的makelfile
clean:
rm –f *.ko *.o *.mod.o *.mod.c *.symvers
endif
模块化编译:
方法1:多个文件编译成一个模块
obj-m+=test.o
test.o–objs= hello.o file1.o file2.o …
**一个模块只能有一个module_init,module_exit,不能有多个入口和出口。
方法2:分别编译对应的模块
obj-m+= file1.o file2.o 或者
obj-m+=file1.o
obj-m+=file2.o
**这种编译方法对入口出口没有限定。
对模块的操作
insmod:加载模块命令,module_init的入口函数被内核调用。
rmmod:下载模块命令,module_exit的出口函数被内核调用。
lsmod:通过读取/proc/modules虚拟文件,列出当前内核加载的模块
modinfo;查看模块的信息
modprobe:根据modules.dep文件检查模块的依赖关系,装载和卸载模块以及
所依赖的模块。(-r 参数卸载模块)
mocprobe会默认到目录/lib/modules/…找modules.dep文件,所以编译好的模块需安装到此目录下(模块的Makefile中添加语句):
make–C /kerneldir/kernel M=/moduledir/mymodule modules_install INSTALL_MOD_PATH=/….installpath/
编译后把安装路径下的内容拷贝至根文件系统的/lib目录
内核编程的注意点:
不允许使用C库;
必须使用GNU C(标C的扩展)
没有内存保护机制
难以执行浮点运算
进程在内核空间执行时,它的内核栈大小只有8K,编程时注意局部变量的大小。内核编程时申请的内存一般都是从堆中分配。
由于内核支持异步中断、抢占和SMP,须时刻注意同步和并发。
内核模块的添加信息:
MODULE_LICENSE(“GPL”) 许可证必须添加
MODULE_AUTHOR(author)
MODULE_VERSION(description)
MODUEL_DEVICE_TABLE(table_info) 模块所支持的设备
MODULE_ALIAS(alter_name)模块别名
内核模块参数:
module_param(var,type,perm):如果权限不为0,模块加载后在/sys/module/ module<name>/parameters/会生成跟变量同名的文件,这个变量的值只能在加载模块是修改。由于/sys的内容存在于内存中,如果大量的模块参数声明并指定权限,势必会占用内存,所以没有特殊需求一般权限写0。
module_param_array(var,type,num,perm)数组,num记录有效的数组元素个数。
内核符号导出:
EXPORT_SYMBOL(函数名或变量)
EXPORT_SYSMBOL_GPL(….)导出的符号只能给遵循GPL协议的模块使用。
printk 打印消息:
<linux/kernel.h> 中定义了8种日志级别用于消息打印。0~7数字越小级别越高。
为了节省CPU资源,提高系统性能,不是所有的内核打印信息都要输出。
cat /proc/sys/kernel/printk查看当前打印级别(可以通过向这个文件写入数字来修改打印级别。或者在内核启动之前通过uboot 设置参数 setnv bootargs指定loglevel的值)
字符设备:以字节流形式顺序访问,例如字符终端(/dev/console)和串口(/dev/ttys0),音频,LCD屏,摄像头和各种传感器等
设备号(32位,dev_t类型)由主设备号(高12位)和次设备号(低20位)构成。
主设备号标识对应的驱动程序,次设备号又内核使用确定具体的设备个体。
它们之间的转换可通过如下宏:
MAJOR(dev_tdev);
MINOR(dev_t dev);
MKDEV(intmajor, int minor);
设备文件的创建
1. 手动创建
mknod /dev/mydev(设备文件名) c(字符设备type) 250(主设备号) 0(次)
2. 自动创建
分配设备号:
方法1静态申请:int register_chrdev_region(dev_t first, int count, char *name)
(需提前知道尚未被使用的设备号,)
方法2动态:int alloc_chrdev_region(dev_t dev,int firstminor,int count,char*name)
分配成功后可通过cat /proc/devices 查看,驱动程序应该使用动态分配设备号。
释放设备号:
void unregister_chrdev_region(dev_tfirst, int count);
字符设备相关数据结构
- 字符设备结构struct cdev:内核中用该结构来表示一个字符设备。
struct cdev {
structfile_operatons *ops;
dev_t dev;
int count;
…
}
- 文件操作结构struct file_operations:函数指针的集合,指向驱动中的函数,这些函数定义了能够对设备进行的操作。
struct file_operations {
struct module*owner;
ssize_t (*read) (…….)
int (*open) (…..)
….
}
- 文件结构struct file :用来描述设备文件打开后状态信息,系统中每个打开的文件在内核空间都有一个关联的struct file。
- inode结构structinode:用于记录文件的物理信息,一个文件可以有多个file结构,但只有一个inode结构
四个结构体的关系
字符设备的分配:
structcdev *my_cdev = cdev_alloc();
my_cdev->ops= &my_fops; //或者直接定义静态全局变量
初始化:
voidcdev_init(struct cdev* cdev,struct file_operations *fpos)
注册:
intcdev_add(struct cdev*dev,dev_t num,unsigned int count)
(字符设备的注册即将my_cdev添加的内核的cdev散列表)
移除:
voidcdev_del(struct cdev *dev)
应用程序访问字符设备驱动
1. 首先安装一个设备对应的驱动
2. 安装过程如:
1) 驱动程序首先分配一个硬件操作方法struct file_operations
2) 然后分配以一个字符设备对象struct cdev
3) 初始化分配的结构体cdev_init
4) 将字符设备注册到内核中cdev_add,以设备号为索引添加到内核的cdev散列表中。
3. 创建对应的设备节点,内核此时会创建这个设备节点对应的inode ,并把设备文件的设备号保存在inode.i_rdev中。根据inode.i_rdev设备号信息,在内核的cdev散列表中找到对应的cdev,然后将这个字符设备结构的指针赋值给inode->i_cdev(用于缓存),以后其他的应用程序打开文件时,直接从inode->i_cdev取出对应的驱动。
4. 应用程序就可以通过open调用打开设备文件
5. open调用C库的open实现,C库的open保存系统调用号到寄存器R7中。
6. open实现最后调用swi指令,触发一个软中断异常。CPU跳转至内核初始化时定义好的的一个异常向量表的入口(0xFFFF0000),软中断的入口是vector_swi,取出open对应的系统调用号,然后在内核定义的系统调用表里找到对应的函数sys_open,执行这个sys_open函数。
7. sys_open函数创建file对象,从驱动中定义的cdev结构中取出fops,将文件操作结构赋值给file的f_op。
8. 判断file->f_op中有没有open实现。有则调用底层驱动的open实现,没有用户空间永远返回为真。