Linux内核开发环境
Table of Contents
Linux简介
Linux基于UNIX开发,UNIX在1969年由Dennis Ritchie和Ken Thompson两位大师开发。 Linux最早由Linus Torvalds于1991年开发。整个Linux系统由系统调用将用户空间和内核空间联系起来,用户通过系统调用来进入到内核态,以完成对设备的访问和控制。因此从功能上讲,内核主要是为用户提供接口,对计算机硬件资源进行管理。
Linux具备一些非常好的技术支持,包括:
- 支持动态载入和卸载内核模块
- 支持SMP
- 抢占式任务调度
- 进程和线程不作区分
- 面向对象设备模型、热插拔支持、用户空间设备文件系统sysfs
构建内核
最新的Linux内核可以通过如下方式获取:
git clone https://github.com/torvalds/linux
可以看到内核下包括如下文件和目录:
Documentation | 文档 |
arch | 架构相关代码 |
block | 块IO层代码 |
crypto | 加密API |
drivers | 设备驱动 |
firmware | 一些驱动会用到的固件 |
fs | 文件系统 |
include | 头文件 |
init | 内核启动代码 |
ipc | 进程间通信代码 |
kernel | 核心子系统,如进程调度 |
lib | 常用库函数 |
mm | 内存管理子系统 |
net | 网络子系统 |
samples | 样本示例代码 |
scripts | 各种脚本 |
security | 安全模块 |
sound | 声音子系统 |
tools | 开发内核实用工具 |
virt | 虚拟化设施 |
COPYING | 许可证 |
CREDITS | 对内核有较多贡献的人列表 |
Kbuild | Kbuild |
Kconfig | Kbuild配置文件 |
MAINTAINERS | 各子系统维护者 |
Makefile | 主Makefile |
在构建内核之前需要对内核进行配置,比如去掉不必要的模块,修改调试级别,如打开动态debug,打开锁调试等等。
make menuconfig
在menuconfig提供的界面中,有如下一些快捷键可以使用:
? | 查看帮助 |
SPC | 用于对当前项选中:星号表示编译至内核、M表示编译为模块 |
ESC | 退出 |
/ |
搜索 |
< > | 左右移动 |
up down | 上下移动 |
配置好内核即可使用如下命令编译安装:
make -j8 # 8 means start 8 threads make modules_install make install
内核调试
动态调试
内核动态调试是通过一个控制文件来控制哪些消息打印出来,哪些消息不打印。要支持这个功能就需要启用如下配置:
- CONFIG_DYNAMIC_DEBUG
如果开启动态调试,那么pr_debug()/dev_dbg(),
print_hex_dump_debug()/print_hex_dump_bytes()就可以动态选择是否打印出来。使用动态调试的好处是,可以控制文件、函数、行号、模块以及格式字符串。控制文件位于<debugfs>/dynamic_debug/control
。
下面是用于控制的样例:
echo 'file svcsock.c line 1603 +p' > control # 控制打印行 echo 'func get_resources +p' > control # 控制函数 echo "file drivers/usb/* +p" > control # 正则表达式控制 cat batch-file > control # 批量控制
控制规则如下:
command ::= match-spec* flags-spec
- match-spec
- 'func' string | 'file' string | 'module' string | 'format' string | 'line' line-range
- func 用函数名匹配
- file 用文件匹配
- module 用模块匹配,不要后缀,其中"-"要替换为"_"
- format 用格式字符串匹配,特殊字符可以用八进制转义得到,也可以用引号包围
- line 根据指定行号范围匹配
- line-range
- lineno | -lineno | lineno- | lineno1-lineno2
- flags-spec
- 更改调试状态
-
移除+
添加=
设置为指定标志p
启用pr_debug()f
在打印消息中添加函数名l
在打印消息中添加行号m
在打印消息中添加模块名t
包含线程ID_
没有任何标志位置起
对于print_hex_dump_debug()和print_hex_dump_bytes(),只有p操作有意义,其它操作被忽略。
我们可以用^[-+=][flmpt_]+$
匹配标志规则部分,要清除所有标志使用=_
即可。
我们也可以从控制文件获取相关信息,控制文件的格式如下:
filename:lineno [module]function flags format
- flags
- 表示启用状态,默认是=_表示没有开启。
如果想查看不是处于默认状态的信息可以用如下一个命令。
awk '$3 != "=_"' control
有时候我们希望在插入模块的时候就打开调试。当执行modprobe foo的时候,modprobe会去为foo.params扫描/proc/cmdline,去掉"foo."之后传递给内核。总共有三个地方的参数会传递给内核,并且有执行顺序。
/etc/modprobe.d/*.conf
options foo dyndbg=+pt options foo dyndbg # defaults to +p
foo.dyndbg as given in boot args
foo.dyndbg=" func bar +p; func buz +mp "
args to modprobe
modprobe foo dyndbg==pmf # override previous settings
这里dyndbg是一个伪参数,每个模块不必自己去定义,相当于系统已经为每个模块定义好了。
打印调试
打印调试可以用到的格式字符如下表所示。
标识符 | 类型 |
---|---|
基本数据类型 | |
%d or %x | int |
%u or %x | unsigned int |
%ld or %lx | long |
%lu or %lx | unsigned long |
%lld or %llx | long long |
%llu or %llx | unsigned long long |
%zu or %zx | size_t |
%zd or %zx | ssize_t |
%p | raw pointer |
函数指针 | |
%pF | versatile_init+0x0/0x110 |
%pf | versatile_init |
%pS | versatile_init+0x0/0x110 |
%pSR | versatile_init+0x9/0x110 |
%ps | versatile_init |
%pB | prev_fn_of_versatile_init+0x88/0x88 |
缓冲区 | 星号用具体的长度值替换 |
%*ph | 00 01 02 … 3f |
%*phC | 0:01:02: … :3f |
%*phD | 0-01-02- … -3f |
%*phN | 00102 … 3f |
物理地址 | |
%pa[p] | 0x01234567 or 0x0123456789abcdef |
DMA地址 | |
%pad | 0x01234567 or 0x0123456789abcdef |
驱动开发
编译环境
设备驱动都放在drivers/
目录下,下面又细分为不同类型的设备驱动。假定要写一个字符设备驱动,需要修改drivers/char/Makefile
以编译新加的驱动。
obj-$(CONFIG_FISHING_POLE) += fishing.o fishing-objs := fishing-main.o fishing-line.o EXTRA_CFLAGS += -DTITANIUM_POLE
由于内核是可以配置的,所以需要添加配置选项,修改drivers/char/Kconfig
:
config FISHING_POLE
depends on EXAMPLE_DRIVERS && !NO_FISHING_ALLOWED
select BAIT
tristate "Fish Master 3000 support"
default n
help
If you say Y here, support for the Fish Master 3000
当然你也可以创建自己的子目录,子目录中Makefile和Kconfig的写法可以参考父目录。
如果只想写一个不加入到内核的驱动,可以这么写Makefile:
obj-m += fishing.o fishing-objs := fishing-main.o fishing-line.o EXTRA_CFLAGS += -DTITANIUM_POLE
编译的时候这么写:
make -C /kernel/source/location SUBDIRS=$PWD modules make modules_install # install module depmod -A # add to dependency
insmod module.ko # insert module rmmod module # remove module modprobe module [parameters] # insert module modprobe –r modules # remove module
设备驱动
Linux对设备分为三种类型,块设备、字符设备、网络设备。块设备以固定块长作为访问单位。字符设备不可寻址,本质上就是字节流。网络设备通过物理适配器提供访问网络的接口。并不是说设备驱动就一定是驱动物理设备,也可是虚拟设备,例如/dev/urandom
就是一个随机数发生器。
设备驱动被写作一个模块,类似于用户空间的一个程序。如果编译时以模块形式生成,那么系统启动后可以动态加载或卸载。一个模块的框架如下所示:
#include <linux/init.h> #include <linux/module.h> static int hello_init(void) /* like main */ { printk(KERN_ALERT "I bear a charmed life.\n"); return 0; } static void hello_exit(void) /* for release resource */ { printk(KERN_ALERT "Out, out, brief candle!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Shakespeare"); MODULE_DESCRIPTION("A Hello, World Module");
一个模块可以有参数,也可以导出接口,添加参数要使用内核提供的module_param
系列接口,导出接口要要使用EXPORT_SYMBOL_GPL
接口。所谓导出接口,就是提供一个函数可以被其它模块使用。
Linux设备驱动模型
基本对象
提到设备模型就不得不说道如下几个类:
- kobject
- 可以看作设备基类,每个设备都应该有一个kobject
- kref
- 在kobject中用kref来进行引用计数,也就是说kref提供了一个通用计数机制
- ktype
- 如果我们把kobject中其它字段看作数据成员,那么ktype就是方法成员
- kset
- 同类对象的集合,ktype是为了让同类对象共享方法,而kset只是一个容器,代表一个子系统
每个kobject都有一个名字和一个引用计数,还有一个父亲,以表示在层次中的位置,此外还有一个类型,一个sysfs中的表示。 kobject自身没有什么用,它们都是嵌入到别的数据结构中去,当然任何数据结构也只能嵌入一个kobject。
struct kobject { const char *name; // 目录名字 struct list_head entry; // head: kset->list struct kobject *parent; // 父对象 struct kset *kset; // 所属集合 struct kobj_type *ktype; // 所属类型 struct kernfs_node *sd; // 关联对象与sysfs struct kref kref; // 引用计数 #ifdef CONFIG_DEBUG_KOBJECT_RELEASE struct delayed_work release; #endif unsigned int state_initialized:1; unsigned int state_in_sysfs:1; unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; // 禁止发送uevent };
uevent
内核空间的设备和驱动信息通过sysfs
文件系统导出到/sys
目录。该目录下各子目录说明如下:
- block
- 系统注册的所有块设备
- bus
- 系统中的总线
- class
- 设备分类,按功能分类
- dev
- 注册的设备节点
- devices
- 导出设备模型
- firmware
- 底层子系统,如ACPI, EDD, EFI等
- fs
- 注册的文件系统
- kernel
- 内核配置和状态信息
- modules
- 载入模块信息
- power
- 电源管理数据
向sysfs添加设备节点是通过kobject来实现的,每一个添加的kobject对应一个目录。而文件则是通过属性添加,可以认为一个文件表示一个属性,添加属性一般要实现show()
和store()
两个方法,用于对文件读取和写入。如果利用好sysfs提供的属性,可以避免使用不安全的ioctl以及混乱的/proc
系统。
内核事件通过uevent发送给用户,而uevent也是通过kobject来发送的。当然要完整的工作,也离不开用户空间的监听程序。当用户插拔设备的时候,内核检测到设备插拔并发出插拔事件,调用/proc/sys/kernel/hotplug
中指定的用户空间应用对事件进行处理。
以device_add为例,该函数的主要工作如下:
- 如果没有名字,设置设备的名字
- 设置其kobj的parent,kobj_add()添加kobject到parent下
- 创建设备sysfs目录下的文件
- uevent
- dev:有设备号才会创建
- device_add_class_symlinks()
- subsystem:位于设备属性下,指向所属的子系统的符号链接。
- device:位于设备属性下,有父亲且不是分区时才会创建,指向父设备的符号链接。
- name:位于子系统属性下,指向设备属性,名字和设备名相同,如果是块设备就不会创建,因为已经在/sys/block下面创建了和设备名相同的符号链接。
- device_add_attrs()
- dev->class->dev_groups
- dev->type->groups
- dev->groups
- dev_attr_online
- bus_add_device()
- device_add_attrs() 添加总线属性,不同于设备属性
- bus->dev_groups
- name:位于总线属性下,指向设备的符号链接
- subsystem:位于设备属性下,指向总线的符号链接
- dpm_sysfs_add()
- 动态PM相关sysfs文件
- device_pm_add() 将设备添加到PM核心链表
- blocking_notifier_call_chain()
kobject_uevent(&dev->kobj, KOBJ_ADD);
- bus_probe_device() 为设备探测合适的驱动
kobject_uevent()
要使用netlink发出uevent,必须配置NET,同样,要使用uevent_helper发出uevent,必须配置UEVENT_HELPER。 udev通过netlink监听,mdev则通过uevent_helper监听。假定所有函数都能成功执行,将其简化后如下所示。
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[]) { struct kobj_uevent_env *env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL); // 利用字符串数组将enum转换为字符串 const char *action_string = kobject_actions[action]; struct kobject *top_kobj = ...; // 必须找到kset struct kset *kset = top_kobj->kset; const struct kset_uevent_ops *uevent_ops = kset->uevent_ops; const char *subsystem; if (kobj->uevent_suppress) // 禁止发出 return 0; if (uevent_ops && uevent_ops->filter) if (!uevent_ops->filter(kset, kobj)) // 被过滤 return 0; if (uevent_ops && uevent_ops->name) subsystem = uevent_ops->name(kset, kobj); else subsystem = kobject_name(&kset->kobj); if (!subsystem) // 必须有子系统 return 0; const char *devpath = kobject_get_path(kobj, GFP_KERNEL); add_uevent_var(env, "ACTION=%s", action_string); add_uevent_var(env, "DEVPATH=%s", devpath); add_uevent_var(env, "SUBSYSTEM=%s", subsystem); for (int i = 0; envp_ext && envp_ext[i]; i++) // 额外环境变量 add_uevent_var(env, "%s", envp_ext[i]); if (uevent_ops && uevent_ops->uevent) // kset操作 uevent_ops->uevent(kset, kobj, env); if (action == KOBJ_ADD) kobj->state_add_uevent_sent = 1; else if (action == KOBJ_REMOVE) kobj->state_remove_uevent_sent = 1; mutex_lock(&uevent_sock_mutex); add_uevent_var(env, "SEQNUM=%llu", // 序列号 (unsigned long long)++uevent_seqnum); #ifdef CONFIG_NET struct uevent_sock *ue_sk; list_for_each_entry(ue_sk, &uevent_sock_list, list) { struct sock *uevent_sock = ue_sk->sk; struct sk_buff *skb; char *scratch; size_t len; if (!netlink_has_listeners(uevent_sock, 1)) continue; len = strlen(action_string) + strlen(devpath) + 2; skb = alloc_skb(len + env->buflen, GFP_KERNEL); scratch = skb_put(skb, len); sprintf(scratch, "%s@%s", action_string, devpath); for (i = 0; i < env->envp_idx; i++) { len = strlen(env->envp[i]) + 1; scratch = skb_put(skb, len); strcpy(scratch, env->envp[i]); } NETLINK_CB(skb).dst_group = 1; netlink_broadcast_filtered(uevent_sock, skb, 0, 1, GFP_KERNEL, kobj_bcast_filter, kobj); } #endif mutex_unlock(&uevent_sock_mutex); #ifdef CONFIG_UEVENT_HELPER if (uevent_helper[0] && !kobj_usermode_filter(kobj)) { struct subprocess_info *info; const char *path = "PATH=/sbin:/bin:/usr/sbin:/usr/bin" add_uevent_var(env, "HOME=/"); add_uevent_var(env, path); init_uevent_argv(env, subsystem); info = call_usermodehelper_setup(env->argv[0], env->argv, env->envp, GFP_KERNEL, NULL, cleanup_uevent_env, env); call_usermodehelper_exec(info, UMH_NO_WAIT); env = NULL; /* freed by cleanup_uevent_env */ } #endif exit: kfree(devpath); kfree(env); return 0; }
向内核提交代码
如果发现内核中存在问题,或者性能可以提高,或者添加新的驱动等待,就可以向内核提交补丁。不过在提交之前必须做好验证工作,首先代码中不能有BUG,代码必须要安照内核标准风格来写,还要做些必要的静态检查等。
关于代码风格,建议先阅读 Linux kernel coding-style。代码风格的检查可以用如下指令检查:
scripts/checkpatch.pl *.patch
如果要检查的不是补丁而是文件,加一个参数-f
即可,如果希望对出现问题的代码修复,可以加参数--fix
或者--fix-inplace
。
sparse是Linux常用的一个静态检查工具,ubuntu用户可以用apt-get安装,安装好之后在调用make
时传递参数C=2
即可。
smatch也是一个静态检查工具,可以通过如下命令获取:
git clone git://repo.or.cz/smatch.git
安装好以后在调用make
时传递参数CHECK="smatch -p=kernel"
即可。
当一切检查妥当之后就可以生成patch,一般采用如下命令生成:
git format-patch --cover-letter --thread --subject-prefix="PATCH v2" -5
如果只需要生成一个commit的patch,是不需要--cover-letter
和--thread
选项的。注意--cover-letter
需要编辑以添加封面信息。如果第一次提交发现有问题,那么在第二次提交的时候就要加上--subject-prefix="PATCH v2"
选项。
准备好patch之后就可以通过如下命令向内核发送补丁了:
git send-email --smtp-server /usr/bin/msmtp \ --from yourname@email.com \ --to maintainer1@email1.com \ --to maintainer2@email2.com \ --cc devel@linuxdriverproject.org \ --cc linux-kernel@vger.kernel.org *.patch
很显然git用到msmtp工具来发送邮件,在ubuntu上可以通过apt-get安装,配置文件在~/.msmtprc
。大致格式如下:
# Set default values for all following accounts. defaults logfile ~/.msmtp.log # gmail account gmail protocol smtp host gmail.com from mickyching@gmail.com user mickyching@gmail.com password PASSWORD port 25 auth ntlm syslog LOG_MAIL # Set a default account account default : gmail