自学内容网 自学内容网

LDD学习2--Scull(TODO)

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 看看代码

scull 使用一块虚拟内存作为模拟设备,并将这块内存划分成多个设备。例如,程序中可以定义 4 个 scull 设备,每个设备都有一个独立的内存区域。每个区域的大小是通过编译时参数或模块参数指定的。

内存分配: scull 的内存分配是通过一个链表实现的,每个链表节点保存了一块内存数据。链表中每个节点的大小是固定的,称为 quantum,多个 quantum 组成一个 qset。当数据超过当前节点的 quantum 大小时,scull 会自动分配新的节点并将其连接到链表中,形成一个灵活的内存管理机制。

核心结构

struct scull_dev

struct scull_dev {
    struct scull_qset *data;  /* Pointer to first quantum set */
    int quantum;              /* the current quantum size */
    int qset;                 /* the current array size */
    unsigned long size;       /* amount of data stored here */
    struct semaphore sem;     /* Mutual exclusion semaphore */
    struct cdev cdev;         /* Char device structure */
};

scull_qset 

struct scull_qset {
    void **data;
    struct scull_qset *next;
};

scull_init_module

这个函数是初始化的函数,

int scull_init_module(void)
{
int result, i;
dev_t dev = 0;

/*
 * Get a range of minor numbers to work with, asking for a dynamic
 * major unless directed otherwise at load time.
 */
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;
}

/* 
 * allocate the devices -- we can't have them static, as the number
 * can be specified at load time
 */
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail;  /* Make this more graceful */
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

        /* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
mutex_init(&scull_devices[i].lock);
scull_setup_cdev(&scull_devices[i], i);
}

        /* At this point call the init function for any friend device */
dev = MKDEV(scull_major, scull_minor + scull_nr_devs);
dev += scull_p_init(dev);
dev += scull_access_init(dev);

#ifdef SCULL_DEBUG /* only when debugging */
scull_create_proc();
#endif

return 0; /* succeed */

  fail:
scull_cleanup_module();
return result;
}

可以看到,初始化的操作就是几个,alloc_chrdev_region,这个用来动态注册设备,kmalloc,这个给自己的主结构分配内存,然后初始化了四个次设备(TODO,这部分还要看看)。另外两个友设备这次就先不看了。。

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

  1. 主设备号(Major Number)

    • 主设备号用于标识设备驱动程序。内核通过主设备号来确定哪个驱动程序会处理对特定设备的请求。
    • 每个设备驱动程序通常会有一个唯一的主设备号,或者一组主设备号,用于它们管理的所有设备。
    • 主设备号通常在驱动程序注册时分配,可以是静态分配的,也可以是动态分配的。
  2. 次设备号(Minor Number)

    • 次设备号用于标识属于同一驱动程序管理的特定设备实例。它允许内核区分同一个驱动程序管理的多个设备。
    • 次设备号可以看作是驱动程序内部的索引,用于区分同一类型的不同设备,如第一个设备、第二个设备等。
    • 次设备号的分配通常由驱动程序控制,可以根据设备的数量和特性来分配。

简单来说,主设备号就是什么设备对应什么驱动,次设备号就是一个驱动可以多个设备。

scull_cleanup_module

这个是退出的清理函数

/*
 * The cleanup function is used to handle initialization failures as well.
 * Thefore, it must be careful to work correctly even if some of the items
 * have not been initialized
 */
void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major, scull_minor);

/* Get rid of our char dev entries */
if (scull_devices) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(scull_devices + i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}

#ifdef SCULL_DEBUG /* use proc only if debugging */
scull_remove_proc();
#endif

/* cleanup_module is never called if registering failed */
unregister_chrdev_region(devno, scull_nr_devs);

/* and call the cleanup functions for friend devices */
scull_p_cleanup();
scull_access_cleanup();

}

然后就是注册回调:

struct file_operations scull_fops = {
.owner =    THIS_MODULE,
.llseek =   scull_llseek,
.read =     scull_read,
.write =    scull_write,
.unlocked_ioctl = scull_ioctl,
.open =     scull_open,
.release =  scull_release,
};

scull_trim():

会在scull_cleanup_module中调用,释放设备占用的内存。 当设备被关闭时调用此函数,它会遍历链表并释放所有分配的内存。

scull_open():

打开设备。 在设备文件被打开时调用,增加设备的引用计数,并进行必要的初始化。

scull_release():

释放设备。 当设备文件被关闭时调用,减少引用计数,如果没有进程再使用该设备,则释放其资源。

scull_read():

从设备读取数据。 将内存中的数据拷贝到用户空间。

scull_write():

向设备写入数据。 将用户空间的数据拷贝到设备的内存中,必要时扩展内存区域。

重点看一下read和write两个函数

scull_read

ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
                loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data; 
struct scull_qset *dptr;/* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;

/* find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;

/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item);

if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */

/* read only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

  out:
mutex_unlock(&dev->lock);
return retval;
}

比较典型的就是mutex_lock_interruptible和mutex_unlock进行锁操作。mutex_lock_interruptible是获得锁,但是可以被信号打断。。

之后就是根据偏移找到正确的数据,并拷贝到用户空间。

scull_write

ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* value used in "goto out" statements */

if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;

/* find listitem, qset index and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;

/* follow the list up to the right position */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
/* write only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

        /* update the size */
if (dev->size < *f_pos)
dev->size = *f_pos;

  out:
mutex_unlock(&dev->lock);
return retval;
}

除了锁机制,然后将用户空间传来的数据拷贝给链表。

scull_ioctl主要是设置参数,这里就不细看了。

《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 字符设备驱动程序的核心技能,并能够理解设备驱动程序在内核中的基本原理和结构。


原文地址:https://blog.csdn.net/fanged/article/details/142408723

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!