scull(Simple Character Utility for Loading Localities),是一个LDD3中完整的字符设备驱动程序示例,代码很经典,值得精读。涉及了内存管理、文件操作、并发处理等内容。
重点内容:
内核内存分配(kmalloc、vmalloc、内核内存映射)。
文件操作接口(read、write、ioctl)。
并发控制(自旋锁、信号量)。
多设备支持(scull 提供多个设备实例)。
这篇主要写了概要,关注代码细节可以参看后面:
1 代码准备和编译
之前我是用的老版的代码,下面这个位置:
examples / Linux Device Drivers 3rd Edition · GitLab
代码确实是原汁原味,但是遇到的问题就是没法编译:

海量的报错把我搞得有点懵圈,后面查了下,要用新版发烧友们的代码:
git clone https://github.com/martinezjavier/ldd3.git
这个弄下来一下就可以了。
在ubuntu的x86设备上是直接可以编过。

在树莓派上面整体编译还是有点问题:

会说有一个sys/io.h找不到。
查了一下是因为这个头文件特供x86平台。ARM这些直接就是不过。
树莓派(以及其他 ARM 架构)不支持
sys/io.h中的直接 I/O 端口操作,因为这是为 x86 架构设计的。你需要检查代码,找出使用了sys/io.h中函数的部分,并移除或替换为 ARM 兼容的操作。如果你使用的是
inp,outp,inb,outb等直接访问 I/O 端口的函数,它们通常只在 x86 系统上使用。在 ARM 设备上,访问外设应该通过内存映射(memory-mapped I/O)或者通过其他 ARM 专用的机制来完成。
好吧,不过不重要,这次毕竟是看scull,这个单独可以编过。就无所谓了。

试了下insmod,也是马上就能出效果。

然后就可以mknod来操作了,这里都要手动去mknod。对了,要chmod和sudo。。。
也可以参考它自己带的脚本
#!/bin/bash
# Sample init script for the scull driver module <rubini@linux.it>
DEVICE="scull"
SECTION="misc"
# The list of filenames and minor numbers: $PREFIX is prefixed to all names
PREFIX="scull"
FILES=" 0 0 1 1 2 2 3 3 priv 16
pipe0 32 pipe1 33 pipe2 34 pipe3 35
single 48 uid 64 wuid 80"
INSMOD=/sbin/insmod; # use /sbin/modprobe if you prefer
function device_specific_post_load () {
true; # fill at will
}
function device_specific_pre_unload () {
true; # fill at will
}
# Everything below this line should work unchanged for any char device.
# Obviously, however, no options on the command line: either in
# /etc/${DEVICE}.conf or /etc/modules.conf (if modprobe is used)
# Optional configuration file: format is
# owner <ownername>
# group <groupname>
# mode <modename>
# options <insmod options>
CFG=/etc/${DEVICE}.conf
# kernel version, used to look for modules
KERNEL=`uname -r`
#FIXME: it looks like there is no misc section. Where should it be?
MODDIR="/lib/modules/${KERNEL}/kernel/drivers/${SECTION}"
if [ ! -d $MODDIR ]; then MODDIR="/lib/modules/${KERNEL}/${SECTION}"; fi
# Root or die
if [ "$(id -u)" != "0" ]
then
echo "You must be root to load or unload kernel modules"
exit 1
fi
# Read configuration file
if [ -r $CFG ]; then
OWNER=`awk "\\$1==\"owner\" {print \\$2}" $CFG`
GROUP=`awk "\\$1==\"group\" {print \\$2}" $CFG`
MODE=`awk "\\$1==\"mode\" {print \\$2}" $CFG`
# The options string may include extra blanks or only blanks
OPTIONS=`sed -n '/^options / s/options //p' $CFG`
fi
# Create device files
function create_files () {
cd /dev
local devlist=""
local file
while true; do
if [ $# -lt 2 ]; then break; fi
file="${DEVICE}$1"
mknod $file c $MAJOR $2
devlist="$devlist $file"
shift 2
done
if [ -n "$OWNER" ]; then chown $OWNER $devlist; fi
if [ -n "$GROUP" ]; then chgrp $GROUP $devlist; fi
if [ -n "$MODE" ]; then chmod $MODE $devlist; fi
}
# Remove device files
function remove_files () {
cd /dev
local devlist=""
local file
while true; do
if [ $# -lt 2 ]; then break; fi
file="${DEVICE}$1"
devlist="$devlist $file"
shift 2
done
rm -f $devlist
}
# Load and create files
function load_device () {
if [ -f $MODDIR/$DEVICE.ko ]; then
devpath=$MODDIR/$DEVICE.ko
else if [ -f ./$DEVICE.ko ]; then
devpath=./$DEVICE.ko
else
devpath=$DEVICE; # let insmod/modprobe guess
fi; fi
if [ "$devpath" != "$DEVICE" ]; then
echo -n " (loading file $devpath)"
fi
if $INSMOD $devpath $OPTIONS; then
MAJOR=`awk "\\$2==\"$DEVICE\" {print \\$1}" /proc/devices`
remove_files $FILES
create_files $FILES
device_specific_post_load
else
echo " FAILED!"
fi
}
# Unload and remove files
function unload_device () {
device_specific_pre_unload
/sbin/rmmod $DEVICE
remove_files $FILES
}
case "$1" in
start)
echo -n "Loading $DEVICE"
load_device
echo "."
;;
stop)
echo -n "Unloading $DEVICE"
unload_device
echo "."
;;
force-reload|restart)
echo -n "Reloading $DEVICE"
unload_device
load_device
echo "."
;;
*)
echo "Usage: $0 {start|stop|restart|force-reload}"
exit 1
esac
exit 0
2 运行
运行稍微有点注意
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ sudo insmod scull.ko
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ dmesg | tail -n 10
[1485344.004463] audit: type=1400 audit(1735679035.710:395): apparmor="DENIED" operation="open" profile="ubuntu_pro_esm_cache//apt_methods" name="/usr/local/lib/libz.so.1.2.13" pid=3459493 comm="https" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
[1485344.014632] audit: type=1400 audit(1735679035.718:396): apparmor="DENIED" operation="open" profile="ubuntu_pro_esm_cache//apt_methods" name="/usr/local/lib/libz.so.1.2.13" pid=3459499 comm="https" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
[1485345.039239] audit: type=1400 audit(1735679036.746:397): apparmor="DENIED" operation="open" profile="ubuntu_pro_esm_cache//apt_methods_gpgv" name="/usr/local/lib/libz.so.1.2.13" pid=3459682 comm="gpgv" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
[1485345.045114] audit: type=1400 audit(1735679036.750:398): apparmor="DENIED" operation="open" profile="ubuntu_pro_esm_cache//apt_methods_gpgv" name="/usr/local/lib/libz.so.1.2.13" pid=3459683 comm="gpgv" requested_mask="r" denied_mask="r" fsuid=0 ouid=0
[1514449.833992] scull: get major 0 0
[1514449.836286] scull: get result 241
[1514449.836835] scullsingle registered at f100008
[1514449.836837] sculluid registered at f100009
[1514449.836838] scullwuid registered at f10000a
[1514449.836839] scullpriv registered at f10000b
网上很多资料,都说创建节点是sudo mknod /dev/scull0 c 240 0,但是这个实际是有问题的。代码如下:
printk(KERN_WARNING "scull: get major %d %d\n", scull_major, scull_minor);
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
"scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
printk(KERN_WARNING "scull: get result %d\n", scull_major);
代码是使用alloc_chrdev_region动态取得主设备号,所以必须要打印出来。从log可以看到我这里是241。所以用下面的:
sudo mknod /dev/scull0 c 241 0
之后就可以正常操作。
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ echo "Hello, SCULL!" > /dev/scull0
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ cat /dev/scull0
Hello, SCULL!
scull从作用来看就是可以缓存用户空间传下去的内容。。。用cat可以打印出来。
另外,我把SCULL_DEBUG调试宏打开了。在proc下面会创建两个节点,scullmem,scullseq。
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ ls /proc/scull*
/proc/scullmem /proc/scullpipe /proc/scullseq
3 编外知识点
1 主次设备号的区别
这里说说主次设备号的区别
主设备号(Major Number):
- 主设备号用于标识设备驱动程序。内核通过主设备号来确定哪个驱动程序会处理对特定设备的请求。
- 每个设备驱动程序通常会有一个唯一的主设备号,或者一组主设备号,用于它们管理的所有设备。
- 主设备号通常在驱动程序注册时分配,可以是静态分配的,也可以是动态分配的。
次设备号(Minor Number):
- 次设备号用于标识属于同一驱动程序管理的特定设备实例。它允许内核区分同一个驱动程序管理的多个设备。
- 次设备号可以看作是驱动程序内部的索引,用于区分同一类型的不同设备,如第一个设备、第二个设备等。
- 次设备号的分配通常由驱动程序控制,可以根据设备的数量和特性来分配。
简单来说,主设备号就是什么设备对应什么驱动,次设备号就是一个驱动可以多个设备。所以这里的次设备号都是0开始。
2 字符设备和块设备
目前的感觉是有点类似TCP和UDP的区别。一个主打精准控制,一个主打量大管饱。
具体代码的区别后面再总结。。。

4 参考
Character device drivers — The Linux Kernel documentation
Simple Linux character device driver – Oleg Kutkov personal blog
《Linux Device Drivers》(LDD)书籍中的 `scull`(Simple Character Utility for Loading Localities)是一个用于演示 Linux 字符设备驱动程序编写的示例代码。它为理解 Linux 内核模块和字符设备驱动程序的编写提供了基础实践平台,帮助开发者了解 Linux 内核中字符设备的工作原理。
### `scull` 的主要作用
`scull` 模块创建了一个虚拟的字符设备,它并不和真实的硬件设备交互,而是将分配的内存空间作为设备存储,这样开发者可以在内存中操作数据,模拟实际的字符设备工作流程。通过这个虚拟设备,LDD 的读者可以学习字符设备驱动的基本结构和关键操作,如文件的打开、关闭、读写、IO 控制(ioctl)等。### `scull` 的关键功能和特点
1. **字符设备驱动**:
`scull` 是一个字符设备驱动程序。字符设备(Character Device)是一种可以顺序读取和写入数据的设备类型,与块设备不同,它没有固定的块大小。2. **虚拟设备**:
`scull` 并不和实际的硬件设备交互,而是在内存中分配一个区域作为虚拟设备。这个区域类似于环形缓冲区或者文件,用户可以通过系统调用 `read()` 和 `write()` 来从该设备中读取和向设备写入数据。3. **多种操作支持**:
`scull` 支持字符设备常见的操作,包括:
- **打开/关闭设备** (`open`, `release`)。
- **读取数据** (`read`)。
- **写入数据** (`write`)。
- **IO 控制命令** (`ioctl`):`scull` 中的 ioctl 函数实现了一些控制命令,用于改变设备的行为。
- **内存管理**:`scull` 还展示了如何实现 `mmap` 来映射设备内存到用户空间。4. **不同类型的 `scull` 设备**:
`scull` 提供了不同的变种,展示了内核模块的灵活性:
- **scull0**:标准的字符设备。
- **scullpipe**:模拟管道设备。
- **scullsingle**:只能单用户打开的设备。
- **sculluid**:每个用户独占的设备。5. **内存管理**:
`scull` 在设备打开时分配内存,并通过不同的方式管理内存。开发者可以学习如何动态分配和释放内存、如何处理内存映射等操作。6. **并发处理**:
`scull` 的代码中展示了如何处理多个进程对同一个设备的并发访问问题,比如如何使用内核中的 **互斥锁**、**信号量** 等同步机制,确保设备操作的安全性。### `scull` 的实现流程
1. **设备的注册和初始化**:
`scull` 模块加载时,注册了一个字符设备驱动,定义了字符设备的主设备号和次设备号。在模块的 `init` 函数中调用 `register_chrdev_region()` 或 `alloc_chrdev_region()` 来动态分配设备号。2. **文件操作接口**:
`scull` 实现了 `file_operations` 结构体中的关键函数,注册到内核中,用户程序通过 `open`, `read`, `write`, `close` 等系统调用与设备交互。这些操作由 `scull_open`, `scull_release`, `scull_read`, `scull_write` 函数实现。3. **数据读写**:
读操作会从内存中读取指定的字节数,写操作则将数据写入到分配的内存中。这个过程模拟了实际硬件设备的数据读写行为。4. **IOCTL 控制**:
`ioctl` 函数允许用户空间程序发送控制命令给内核模块,改变设备的行为。这部分通常用于设备的特殊功能控制,`scull` 中展示了如何处理和定义这些控制命令。5. **模块的卸载**:
`scull` 模块被卸载时,调用 `unregister_chrdev_region()` 函数注销设备号,释放设备分配的内存。### 使用 `scull` 示例的意义
通过 `scull`,开发者可以学习并掌握以下内容:
- 字符设备驱动的开发流程。
- 如何处理内核中的文件操作(如 `open`, `read`, `write`, `ioctl` 等)。
- 如何管理内核空间的内存(动态分配和释放)。
- 如何处理并发访问问题。
- 如何使用内核日志系统(如 `printk()`)进行调试。
- 模块的加载与卸载过程。### 总结
`scull` 是 LDD 中用于教学的字符设备驱动程序,虽然它是一个虚拟设备,但它涵盖了字符设备驱动开发的方方面面。通过 `scull`,读者可以掌握编写 Linux 字符设备驱动程序的核心技能,并能够理解设备驱动程序在内核中的基本原理和结构。


3540

被折叠的 条评论
为什么被折叠?



