操作系统
一、程序分类
程序按其运行环境分为:
-
裸机程序:直接运行在对应硬件上的程序
-
应用程序:只能运行在对应操作系统上的程序
二、计算机系统的层次结构
计算机系统两种层次结构:
2.1 无操作系统的简单的两层结构
2.2 有操作系统的复杂的四层结构
三、 什么是操作系统
狭义的操作系统:给应用程序提供运行环境的裸机程序,也被称为操作系统内核
广义的操作系统:一组软件集合,它包含:
-
最核心的一个裸机程序 :内核 (kernel)
-
app开发常用的一些功能库(如:C语言标准函数库、线程库、C++标准类库、QT类库等等)
-
一些管理用的特殊app(如桌面、命令行、app包管理器、资源管理器、系统设置、一些常用后台服务程序)
四、操作系统内核的实现模式
内核:操作系统最核心的那个裸机程序,主要负责硬件资源的驱动和管理。
一个操作系统内核主要包括如下几个子模块:
-
任务(进/线程)管理:多任务支持、任务调度、任务间通讯
-
内存管理:物理内存管理,虚拟内存实现
-
设备驱动:各种外部设备的I/O支持
-
网络协议支持
-
文件系统支持
-
启动管理
两种典型的内核实现模式:
-
单内核(宏内核):所有子模块代码编译到一个比较大的可执行文件(镜像文件)中,各子模块代码共用同一套运行资源,各模块间的交互直接通过函数调用来进行
-
微内核:只将任务管理、内存管理、启动管理最基本的三个子模块编译到一个微型的可执行文件中,其它子模块则各自编译成独立的后台服务程序,这些服务程序与微型内核以及app间主要通过各种IPC进行通讯
单内核特点:效率高,稳定性低,扩展性差,安全性高,典型操作系统:UNIX系列、Linux
微内核特点:效率低,稳定性高,扩展性高,安全性低,典型操作系统:Windows,QNX
五、什么是设备驱动程序
驱动(Driver)设备驱动(Device Driver)
一种添加到操作系统中的特殊程序,主要作用是协助操作系统完成应用程序与对应硬件设备之间数据传送的功能
简言之,设备驱动程序就是操作系统中“驱动”对应硬件设备使之能正常工作的代码。
一个驱动程序主要完成如下工作:
-
初始化设备,让设备做好开始工作的准备
-
读数据:将设备产生的数据传递给上层应用程序
-
写数据:将上层应用程序交付过来的数据传递给设备
-
获取设备信息:协助上层应用程序获取设备的属性、状态信息
-
设置设备信息:让上层应用程序可以决定设备的一些工作属性、模式
-
其它相关操作:如休眠、唤醒、关闭设备等
其中最核心的工作就是设备数据的输入和输出,因此计算机外部设备(外设)也被称为IO设备
驱动开发学习
学习Linux驱动开发的前提条件
1. 精通C语言
2. 能看懂硬件原理图
3. 敢阅读芯片手册(datasheet)
4. 有Linux操作系统的一些背景知识,比如:
a. Linux常用命令的使用
b. Linux常用系统调用函数的编程,尤其是IO相关函数(open、close、read、write等)
环境搭建
开发板运行Linux需要的原料
- bootloder程序:u-boot-fs4412.bin ---> 烧写到SD卡 作为u盘启动盘
- uImage ---tftp---> 传输到开发板内存
- 设备树文件:Exynos4412-fs4412.dtb ---tftp---> 传输到开发板内存
- rootfs.tar.xz ---生成--->rootfs根文件系统
1.1 u-boot-fs4412.bin
开机运行的第一个裸机程序被称为bootloader,主要负责:
-
加载内核可执行文件到内存运行
-
给待运行的内核准备好启动参数
-
加载二进制设备树文件到内存
-
安装系统
u-boot是一个开源的bootloader程序,u-boot-fs4412.bin由其源码编译生成。
1.2 uImage
Linux内核的裸机可执行文件,由Linux源码编译生成。
uImage是U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的“头”,说明这个映像文件的类型、加载位置、生成时间、大小等信息。换句话说,如果直接从uImage的0x40位置开始执行,zImage和uImage没有任何区别。
1.3 exynos4412-fs4412.dtb
ARM-Linux内核启动、运行过程中需要一些来自各芯片手册的编程依据,该文件专门用于记录这些依据
设备树文件有两种格式:
-
.dts、.dtsi:文本形式,便于书写、修改
-
.dtb:二进制形式,由.dts文件经专门工具处理后生成
1.4 rootfs.tar.xz
Linux内核运行成功后,需要运行第一个应用程序(即祖先进程)以及后续其它应用程序
而任何应用程序的运行需要各种文件的支持,如:可执行文件、库文件、配置文件、资源文件
这些文件的持久保存和按路径访问需要外存分区特定文件系统的支持
rootfs就是Linux系统根目录所在的分区,其内包含根分区下众多常用app所需的文件。
rootfs.tar.xz文件是根分区打包生成的压缩文件
这里ubuntu版本统一为ubuntu14.04 32位版
在ubuntu系统登录用户家目录下创建文件夹fs4412,用于存放后面安装环境所用的所有文件,过程如下:
1. cd ~ 2. mkdir fs4412 3. 将uImage u-boot-fs4412.bin exynos4412-fs4412.dtb gcc-4.6.4.tar.xz mkimage rootfs.tar.xz sdfuse_q.zip linux-3.14-fordriver.tgz等8个文件传到~/fs4412目录下备用
交叉工具链的安装
cd ~/fs4412
sudo tar xvf gcc-4.6.4.tar.xz -C /opt
cd /opt/gcc-4.6.4/bin
pwd
#复制pwd命令的输出结果 ------ 完整的绝对路径 ( /opt/gcc-4.6.4/bin )
cd ~
vim .bashrc
#在.bashrc文件的最后一行添加:export PATH=$PATH:第6步复制的路径
#保存退出.bashrc
. .bashrc #让第9、10步的修改生效
arm加两次tab键,能看到一坨的arm-none-linux开头的显示则说明安装成功
tftp安装
#安装tftp-hpa tftpd-hpa:
sudo apt-get install tftp-hpa tftpd-hpa
sudo mkdir /tftpboot #创建tftp服务端共享目录
sudo chmod -R 777 /tftpboot #修改目录权限
#修改服务端配置文件---tftpd-hpa
sudo vim /etc/default/tftpd-hpa
#文件内容如下:
#RUN_DAEMON="no"
#OPTIONS="-s /tftpboot -c -p -U tftpd"
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/tftpboot"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="-l -c -s"
#运行服务端
sudo service tftpd-hpa restart
#测试
cd /tftpboot
touch xxx
vim xxx #随便输入一些内容后保存退出
cd ~
tftp 127.0.0.1
tftp>get xxx
tftp>q
cat xxx #查看xxx内容为上面输入的内容则表示安装成功,否则安装过程有问题
NFS安装
#nfs 安装
sudo apt-get install nfs-kernel-server
#编辑服务端配置文件----/etc/exports
sudo vim /etc/exports
#在其内添加一行,内容如下:
/opt/4412/rootfs *(rw,sync,no_root_squash,no_subtree_check)
#创建挂载点目录并修改挂载点目录的访问权限
sudo mkdir /opt/4412/rootfs -p
sudo chmod 777 /opt/4412/rootfs
#启动NFS服务端(每一次修改/etc/exports都要重启nfs)
sudo service nfs-kernel-server restart
sudo service rpcbind restart
#验证安装是否正确
#在/opt/4412/rootfs下创建一个空文件
cd /opt/4412/rootfs
touch test
sudo mount 127.0.0.1:/opt/4412/rootfs /mnt
#127.0.0.1(这是被挂目录的主机IP)
#ubuntu上NFS服务器上被挂目录的绝对路径/opt/4412/rootfs
#/mnt(挂载的目的地)
ls -l /mnt #如果有test的话就说明ok了
sudo rm /mnt/test
sudo mount /mnt #卸掉挂载的目录
制作SD卡启动盘
方法1:在Linux下制作
一、准备好烧录脚本
cd ~/fs4412
unzip sdfuse_q.zip
cd sdfuse_q
chmod +x *.sh
二、将SD卡插入USB读卡器,并连接到虚拟机
或者
三、烧录
cp ../u-boot-fs4412.bin .
sudo ./mkuboot.sh #烧录
#原理说明
#dd if=u-boot-fs4412.bin of=/dev/sdb seek=1
方法2:在Windows下制作
ubuntu Linux下执行以下命令制作u-boot-fs4412.img
cd ~/fs4412
mkdir win-sd
cp ./u-boot-fs4412.bin ./win-sd
cd win-sd
dd if=/dev/zero of=sector0 bs=512 count=1
cat sector0 u-boot-fs4412.bin > u-boot-fs4412.img
将u-boot-fs4412.img文件传到windows下,放到一个路径不含任何中文的目录下 windows下解压“SD卡烧写.rar”文件 解压后双击运行其中的Win32DiskImager.exe来烧写u-boot-fs4412.img到SD卡(步骤见下图)
-
将SD卡插入卡槽或者将SD卡插入读卡器后将读卡器插入USB接口
-
运行Win32DiskImager.exe,在上图1处看能不能识别出SD卡的盘符,如不能请检查连接情况
-
点击2处按钮,在出现的窗口里找到并选中u-boot-fs4412.img文件
-
点击3处Write按钮开始烧写并等待完成后安全拔出SD卡
串口终端设置
安装TeraTerm串口终端软件,安装过程:一路下一步
将USB转串口线插入电脑USB接口
双击运行TeraTerm选择串口后点击确定:
设置串口通讯参数:
设置字体:
验证串口连接和制作好SD卡:
-
USB转串线9针端连接开发板三个9孔母口的COM2(中间的那个)
-
开发板启动模式开关设置为下图形式
-
开发板插入电源,打开开关,观察串口终端软件界面有没有正常内容显示,没有则认真检查前面的操作
-
u-boot参数设置
串口终端软件界面下,给开发板加电,刚加电时有几秒的倒计时,
在倒计时时间内,敲空格键可以进入u-boot命令行
在u-boot命令行下一次执行如下u-boot命令:
u-boot# setenv serverip 192.168.9.16
u-boot# setenv ipaddr 192.168.9.99
u-boot# setenv gatewayip 192.168.9.1
u-boot# setenv bootcmd tftp 41000000 uImage\;tftp 42000000 exynos4412-fs4412.dtb\;bootm 41000000 - 42000000
u-boot# setenv bootargs root=/dev/nfs nfsroot=192.168.9.16:/opt/4412/rootfs rw console=ttySAC2,115200 init=/linuxrc ip=192.168.9.99
u-boot# saveenv
网线连接开发板和主机,验证双方网络是否畅通
主机侧网络设置:
先关闭虚拟ubuntu系统
开发板侧-----串口终端软件界面
u-boot# ping 192.168.9.16
#出现is alive表示网络畅通,否则检查网线连接和网络设置
开发板运行Linux
1. 网线连接开发板和主机
2. ubuntu下拷贝uImage、exynos4412-fs4412.dtb两个文件到/tftpboot目录下
cd ~/fs4412
cp uImage exynos4412-fs4412.dtb /tftpboot
3. rootfs.tar.xz解压到/opt/4412
sudo tar xvf rootfs.tar.xz -C /opt/4412
sudo chmod 777 /opt/4412/rootfs
4. 启动tftp服务
sudo service tftpd-hpa restart
5. 开发板加电,观察串口终端软件界面,看能不能进入Linux命令行
内核编译
sudo apt-get install libncurses5-dev #如已安装则跳过本步
cd ~/fs4412
sudo cp ./mkimage /sbin
sudo chmod 777 /sbin/mkimage
tar zxvf linux-3.14-fordriver.tgz
cd linux-3.14
make fs4412_defconfig
make uImage -j2 #有代码变更需重新生成uImage时,执行本步骤
#将在arch/arm/boot目录下生成uImage文件,拷贝uImage到/tftpboot下启动开发板可以验证uImage的正确性
make dtbs #设备树源文件被更改需重新生成dtb文件时,执行本步骤
#将在arch/arm/boot/dts目录下生成exynos4412-fs4412.dtb文件
#拷贝exynos4412-fs4412.dtb到/tftpboot下启动开发板可以验证exynos4412-fs4412.dtb的正确性
内核模块添加功能
1.静态加载法
即新功能源码与内核其它代码一起编译进uImage文件内
-
新功能源码与Linux内核源码在同一目录结构下
在linux-3.14/driver/char/目录下编写myhello.c,文件内容如下
#include <linux/module.h>
#include <linux/kernel.h>
int __init myhello_init(void)
{
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("myhello is running\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
return 0;
}
void __exit myhello_exit(void)
{
printk("myhello will exit\n");
}
MODULE_LICENSE("GPL");
module_init(myhello_init);
module_exit(myhello_exit);
2.给新功能代码配置Kconfig
#进入myhello.c的同级目录
cd ~/fs4412/linux-3.14/drivers/char
vim Kconfig
#39行处添加如下内容:
config MY_HELLO
tristate "This is a hello test"
help
This is a test for kernel new function
3.给新功能代码改写Makefile
#进入myhello.c的同级目录
cd ~/fs4412/linux-3.14/drivers/char
vim Makefile
#拷贝18行,粘贴在下一行,修改成:
obj-$(CONFIG_MY_HELLO) += myhello.o
注:CONFIG_后的名字要和第二步中的config定义的命名保持一致 8
4.make menuconfig 界面里将新功能对应的那项选择成<*>
cd ~/fs4412/linux-3.14
make menuconfig
#make menuconfig如果出错,一般是两个原因:
#1. libncurses5-dev没安装
#2. 命令行界面太小(太矮或太窄或字体太大了)
5.make uImage
6.cp arch/arm/boot/uImage /tftpboot
7.启动开发板观察串口终端中的打印信息
2 动态加载法
即新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko
a、新功能源码与Linux内核源码在同一目录结构下时
-
给新功能代码配置Kconfig
-
给新功能代码改写Makefile
-
make menuconfig 界面里将新功能对应的那项选择成<M>
-
make uImage
-
cp arch/arm/boot/uImage /tftpboot
-
make modules
make modules会在新功能源码的同级目录下生成相应的同名.ko文件(生成的ko文件只适用于开发板linux)
注意:此命令执行前,开发板的内核源码(uImage)已被编译。
b、新功能源码与Linux内核源码不在同一目录结构下时
-
cd ~/fs4412
-
mkdir mydrivercode
-
cd mydrivercode
-
cp ../linux-3.14/drivers/char/myhello.c .
-
vim Makefile
-
make (生成的ko文件适用于主机ubuntu linux)
-
make ARCH=arm (生成的ko文件适用于开发板linux,注意此命令执行前,开发板的内核源码已被编译)
#file命令可以查看指定ko文件适用于哪种平台,用法:
file ko文件
#结果带x86字样的适用于主机ubuntu linux,带arm字样的适用于开发板linux
c、主机ubuntu下使用ko文件
sudo insmod ./???.ko #此处为内核模块文件名,将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod #查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
sudo rmmod ??? #,此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
sudo dmesg -C #清除内核已打印的信息
dmesg #查看内核的打印信息
d、开发板Linux下使用ko文件
#先将生成的ko文件拷贝到/opt/4412/rootfs目录下:
cp ????/???.ko /opt/4412/rootfs
#在串口终端界面开发板Linux命令行下执行
insmod ./???.ko #将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod #查看已被插入的内核模块有哪些
rmmod ??? #将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
内核随时打印信息,我们可以在串口终端界面随时看到打印信息,不需要dmesg命令查看打印信息
内核模块编程
内核模块基础代码解析
Linux内核的插件机制——内核模块module
类似于浏览器、eclipse这些软件的插件开发,Linux提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。
主要解决:
-
单内核扩展性差的缺点
-
减小内核镜像文件体积,一定程度上节省内存资源
-
提高开发效率
-
不能彻底解决稳定性低的缺点:内核模块代码出错可能会导致整个系统崩溃
内核模块的本质:一段隶属于内核的“动态”代码,与其它内核代码是同一个运行实体,共用同一套运行资源,只是存在形式上是独立的。
#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE
/*该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作
被称为模块的入口函数
__init的作用 :
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init myhello_init(void)
{
/*内核是裸机程序,不可以调用C库中printf函数来打印程序信息,
Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel)
printk不支持浮点数打印*/
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("myhello is running\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
return 0;
}
/*该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作
被称为模块的出口函数
__exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit myhello_exit(void)
{
printk("myhello will exit\n");
}
/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2" "GPL and additional rights" "Dual BSD/GPL" "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用
其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
myhello:module license 'unspecified' taints kernel
Disabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");
/*
module_init 宏
1. 用法:module_init(模块入口函数名)
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(myhello_init);
/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(myhello_exit);
模块三要素:入口函数 出口函数 MODULE__LICENSE
内核模块的多源文件编程
makefile:
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= 目标板linux内核源码顶层目录的绝对路径
ROOTFS ?= 目标板根文件系统顶层目录的绝对路径
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions
else
obj-m += hello.o
endif
Makefile中:
obj-m用来指定模块名,注意模块名加.o而不是.ko
可以用 模块名-objs 变量来指定编译到ko中的所有.o文件名(每个同名的.c文件对应的.o目标文件)
一个目录下的Makefile可以编译多个模块:
添加:obj-m += 下一个模块名.o
内核模块信息宏
MODULE_AUTHOR(字符串常量); //字符串常量内容为模块作者说明
MODULE_DESCRIPTION(字符串常量); //字符串常量内容为模块功能说明
MODULE_ALIAS(字符串常量); //字符串常量内容为模块别名
这些宏用来描述一些当前模块的信息,可选宏
这些宏的本质是定义static字符数组用于存放指定字符串内容,这些字符串内容链接时存放在.modinfo字段,可以用modinfo命令来查看这些模块信息,用法:
modinfo 模块文件名
模块传参
module_param(name,type,perm);//将指定的全局变量设置成模块参数
/*
name:全局变量名
type:
使用符号 实际类型 传参方式
bool bool insmod xxx.ko 变量名=0 或 1
invbool bool insmod xxx.ko 变量名=0 或 1
charp char * insmod xxx.ko 变量名="字符串内容"
short short insmod xxx.ko 变量名=数值
int int insmod xxx.ko 变量名=数值
long long insmod xxx.ko 变量名=数值
ushort unsigned short insmod xxx.ko 变量名=数值
uint unsigned int insmod xxx.ko 变量名=数值
ulong unsigned long insmod xxx.ko 变量名=数值
perm:给对应文件 /sys/module/name/parameters/变量名 指定操作权限 一般写0664
#define S_IRWXU 00700
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100
#define S_IRWXG 00070
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010
#define S_IRWXO 00007
#define S_IROTH 00004
#define S_IWOTH 00002 //不要用 编译出错
#define S_IXOTH 00001
*/
module_param_array(name,type,&num,perm);
/*
name、type、perm同module_param,type指数组中元素的类型
&num:存放数组大小变量的地址,可以填NULL(确保传参个数不越界)
传参方式 insmod xxx.ko 数组名=元素值0,元素值1,...元素值num-1
*/
示例代码:
#include <linux/module.h>
#include <linux/kernel.h>
int gx = 10;
char *gstr = "hello";
int gary[5] = {1,2,3,4,5};
module_param(gx,int,0664);
module_param(gstr,charp,0664);
module_param_array(gary,int,NULL,0664);
int __init testparam_init(void){
int i=0;
printk("gx = %d\n",gx);
printk("gstr = %s\n",gstr);
for(i=0;i<5;i++){
printk("%d ",gary[i]);
}
printk("\n");
return 0;
}
void __exit testparam_exit(void){
printk("testparam will exit\n");
}
MODULE_LICENSE("GPL");
module_init(testparam_init);
module_exit(testparam_exit);
Makefile中要添加:obj-m += testparam.o
可用MODULE_PARAM_DESC宏对每个参数进行作用描述,用法:
MODULE_PARM_DESC(变量名,字符串常量);
字符串常量的内容用来描述对应参数的作用
modinfo可查看这些参数的描述信息
模块依赖
既然内核模块的代码与其它内核代码共用统一的运行环境,也就是说模块只是存在形式上独立,运行上其实和内核其它源码是一个整体,它们隶属于同一个程序,因此一个模块或内核其它部分源码应该可以使用另一个模块的一些全局特性。
一个模块中这些可以被其它地方使用的名称被称为导出符号,所有导出符号被填在同一个表中这个表被称为符号表。
最常用的可导出全局特性为全局变量和函数
查看符号表的命令:nm nm查看elf格式的可执行文件或目标文件中包含的符号表,用法:
nm 文件名
(可以通过man nm查看一些字母含义)
两个用于导出模块中符号名称的宏:
- EXPORT_SYMBOL(函数名或全局变量名)
- EXPORT_SYMBOL_GPL(函数名或全局变量名) 需要GPL许可证协议验证
使用导出符号的地方,需要对这些符号进行extern声明后才能使用这些符号
B模块使用了A模块导出的符号,此时称B模块依赖于A模块,则:
-
编译次序:先编译模块A,再编译模块B,当两个模块源码在不同目录时,需要:
-
先编译导出符号的模块A
-
拷贝A模块目录中的Module.symvers到B模块目录
-
编译使用符号的模块B。否则编译B模块时有符号未定义错误
-
-
加载次序:先插入A模块,再插入B模块,否则B模块插入失败
-
卸载次序:先卸载B模块,在卸载A模块,否则A模块卸载失败
当两个依赖文件在不同目录下,要先编译提供全局变量的文件,然后将生成的 .symvers复制到另一个文件的同级目录,才能成功编译。
补充说明: 内核符号表(直接当文本文件查看) /proc/kallsyms运行时 /boot/System.map编译后
内核空间和用户空间
为了彻底解决一个应用程序出错不影响系统和其它app的运行,操作系统给每个app一个独立的假想的地址空间,这个假想的地址空间被称为虚拟地址空间(也叫逻辑地址),操作系统也占用其中固定的一部分,32位Linux的虚拟地址空间大小为4G,并将其划分两部分:
-
0~3G 用户空间 :每个应用程序只能使用自己的这份虚拟地址空间
-
3G~4G 内核空间:内核使用的虚拟地址空间,应用程序不能直接使用这份地址空间,但可以通过一些系统调用函数与其中的某些空间进行数据通信
实际内存操作时,需要将虚拟地址映射到实际内存的物理地址,然后才进行实际的内存读写
执行流
执行流:有开始有结束总体顺序执行的一段独立代码,又被称为代码上下文
计算机系统中的执行流的分类:
执行流:
-
任务流--任务上下文(都参与CPU时间片轮转,都有任务五状态:就绪态 运行态 睡眠态 僵死态 暂停态)
1. 进程
2. 线程
1. 内核线程:内核创建的线程
2. 应用线程:应用进程创建的线程
2. 异常流--异常上下文
1. 中断
2. 其它异常应用编程可能涉及到的执行流:
1. 进程
2. 线程内核编程可能涉及到的执行流:
1. 应用程序自身代码运行在用户空间,处于用户态 ----------------- 用户态app
2. 应用程序正在调用系统调用函数,运行在内核空间,处于内核态,即代码是内核代码但处于应用执行流(即属于一个应用进程或应用线程) ---- 内核态app
3. 一直运行于内核空间,处于内核态,属于内核内的任务上下文 --------- 内核线程
4. 一直运行于内核空间,处于内核态,专门用来处理各种异常 --------- 异常上下文
模块编程与应用编程的比较
不同点 | 内核模块 | 应用程序 |
---|---|---|
API来源 | 不能使用任何库函数 | 各种库函数均可以使用 |
运行空间 | 内核空间 | 用户空间 |
运行权限 | 特权模式运行 | 非特权模式运行 |
编译方式 | 静态编译进内核镜像或编译特殊的ko文件 | elf格式的应用程序可执行文件 |
运行方式 | 模块中的函数在需要时被动调用 | 从main开始顺序执行 |
入口函数 | init_module | main |
退出方式 | cleanup_module | main函数返回或调用exit |
浮点支持 | 一般不涉及浮点运算,因此printk不支持浮点数据 | 支持浮点运算,printf可以打印浮点数据 |
并发考虑 | 需要考虑多种执行流并发的竞态情况 | 只需考虑多任务并行的竞态 |
程序出错 | 可能会导致整个系统崩溃 | 只会让自己崩溃 |
内核接口头文件查询
大部分API函数包含的头文件在include/linux目录下,因此:
-
首先在include/linux 查询指定函数:grep 名称 ./ -r -n
-
找不到则更大范围的include目录下查询,命令同上