LDD学习2--Scull基本介绍

scull(Simple Character Utility for Loading Localities),是一个LDD3中完整的字符设备驱动程序示例,代码很经典,值得精读。涉及了内存管理、文件操作、并发处理等内容。
重点内容:
内核内存分配(kmalloc、vmalloc、内核内存映射)。
文件操作接口(read、write、ioctl)。
并发控制(自旋锁、信号量)。
多设备支持(scull 提供多个设备实例)。

这篇主要写了概要,关注代码细节可以参看后面:

LDD3学习5--Scull2-CSDN博客

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 主次设备号的区别

这里说说主次设备号的区别

  1. 主设备号(Major Number)

    • 主设备号用于标识设备驱动程序。内核通过主设备号来确定哪个驱动程序会处理对特定设备的请求。
    • 每个设备驱动程序通常会有一个唯一的主设备号,或者一组主设备号,用于它们管理的所有设备。
    • 主设备号通常在驱动程序注册时分配,可以是静态分配的,也可以是动态分配的。
  2. 次设备号(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 字符设备驱动程序的核心技能,并能够理解设备驱动程序在内核中的基本原理和结构。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值