1.深入剖析Linux文件系统之文件系统挂载(一)(超详细~)
2.用 BusyBox 构建根文件系统
3.一文搞懂Linux内核网络设备驱动(白嫖小知识~)
4.Linux 系统内核概述
5.字符设备中的几个函数分析
6.Linux内核源码解析---mount挂载原理
深入剖析Linux文件系统之文件系统挂载(一)(超详细~)
深入剖析Linux文件系统之文件系统挂载(一)(超详细~) 我们知道,在Linux系统中,将一个块设备上的文件系统挂载到特定目录才能访问该文件系统下的文件。本文将详细阐述文件系统挂载的核心逻辑,包括Linux内核为挂载文件系统所执行的操作以及为何必须挂载才能访问文件。本文分为上下两篇,好看APP下载源码上篇着重于挂载全貌及具体文件系统挂载方法,下篇则详细介绍挂载实例与挂载点、超级块的关系。 在Linux中,虚拟文件系统层VFS通过统一所有具体文件系统的接口,屏蔽差异,向用户提供一致的访问方式。VFS作为接口层,向下连接具体的文件系统,向上提供用户进程访问文件的功能。接下来,我们探讨VFS中几个关键对象的作用。 VFS对象包括: file_system_type:描述文件系统类型,包括磁盘文件系统、内存文件系统、伪文件系统和网络文件系统。磁盘文件系统用于非易失性存储介质上的文件,如ext2、ext4、xfs等;内存文件系统在内存上存储文件;伪文件系统则是内核可见或用户可见的虚拟文件系统,如proc、sysfs等;网络文件系统允许访问远程计算机上的数据。 super_block:用于描述块设备上文件系统整体信息,如文件块大小、最大文件大小、文件系统标识等。磁盘文件系统仅有一个super_block描述整个文件系统。 mount:描述超级块与挂载点之间的联系,建立文件系统挂载的实例。磁盘文件系统可被多次挂载,每次挂载内存中创建一个mount对象。 inode:描述磁盘上文件的元数据,文件系统需要从块设备读取磁盘上的inode,创建内存中的inode对象,通常在文件首次打开时创建。2023码支付源码 dentry:用于描述文件层次结构,构建目录树,存储目录或文件的名称和inode号,以便进程访问目录项。 file:描述进程打开的文件,创建文件对象加入进程的文件打开表,通过文件描述符进行读写操作。 挂载流程包括系统调用处理、挂载点路径查找、参数合法性检查、调用具体文件系统挂载方法、以及实例添加到全局文件系统树。挂载实例添加到全局文件系统树涉及vfs_get_tree和do_new_mount_fc函数,ext2对挂载的处理则包括初始化阶段、挂载时调用、以及通过mount_bdev执行实际挂载工作。 具体文件系统挂载方法包括: ext2对挂载的处理:启动阶段初始化,挂载时调用ext2_mount,执行mount_bdev来执行实际挂载,ext2_fill_super读取磁盘上的超级块并填充内存中的超级块。 mount_bdev源码分析:查找块设备描述符,创建或获取vfs超级块,调用具体文件系统的fill_super方法读取并填充超级块。 ext2_fill_super源码分析:读取磁盘上的超级块,填充并关联vfs超级块,读取块组描述符,读取磁盘根inode并建立根inode,创建根dentry关联到根inode。 挂载完成后,文件系统已准备好被访问,用户进程通过文件路径打开文件,但尚未关联至挂载点。为了将文件系统关联到挂载点,需要通过do_new_mount_fc将挂载实例加入全局文件系统树。下篇将详细讲解这一过程。用 BusyBox 构建根文件系统
构建Linux嵌入式系统的基石是根文件系统,它是一个集成核心组件的单一目录,为后续软件和设备管理提供基础。根文件系统内包含了诸如/bin的系统命令(strong>如ls、cd等),bjl路单源码/dev管理设备,/etc配置文件以设置环境,/lib存放必要库文件,/mnt用于临时挂载,/proc虚拟系统信息确保系统运行透明,/usr为软件资源库,/var存储可变数据,而/sbin则包含管理员工具,/sys用于设备管理和监控,/opt则存放可选软件,sysfs和sysfs类似但功能略有差异。 BusyBox,这个强大的瑞士军刀工具,扮演着构建根文件系统的关键角色。首先,从官网下载适合的版本,如busybox-1..0,并在Ubuntu虚拟机中借助NFS服务进行定制。这里,我们需要确保在Makefile中针对目标架构进行适当的调整,尤其是处理可能的COMPILE错误,使用绝对路径,并解决中文字符问题,比如在源码中的printable_string.c和unicode.c文件中,可能需要注释或调整字符编码规则以支持中文显示。 定制BusyBox的过程可通过两种方式完成:defconfig(默认配置)或图形化的menuconfig。推荐动态编译,并激活mdev和Unicode支持,以确保兼容性和功能性。 编译步骤如下:首先运行make defconfig 或 make menuconfig,然后选择动态编译和必要的Unicode支持。接着,使用make make install CONFIG_PREFIX=/path 命令将编译后的工具和文件安装到指定的rootfs目录,这里会生成bin、sbin、usr和linuxrc文件夹,其中Linux内核通过寻找init程序(通常是linuxrc)进入用户态。 接下来,为了增强根文件系统的功能性,我们需要添加lib库。日内t指标源码从交叉编译器的/usr/local/arm/gcc-linaro-...目录下的arm-linux-gnueabihf/libc/lib子目录中复制.so和.a文件到rootfs/lib,特别注意处理特殊库文件ld-linux-armhf.so.3。 除了基本的文件夹结构,如dev、proc、mnt、sys、tmp和root,还需要创建额外的目录以支持系统的完整功能。例如,dev目录用于设备文件管理,proc用于虚拟系统信息,mnt用于挂载外部存储,sys用于设备驱动的配置,而tmp则存放临时文件。 最后,通过NFS服务将rootfs挂载到开发板上,确保在bootargs中正确设置root,例如:root=/dev/nfs, nfsroot=...:/home/andyxi/linux/nfs/rootfs, proto=tcp, rw。然后,通过串口设置bootargs启动Linux,如果出现错误,表明rootfs可能还不完整,后续我们将深入探讨如何修复和完善这个关键步骤。 获取BusyBox的具体资源,请关注相关渠道并输入关键词"busybox"获取详细信息。一文搞懂Linux内核网络设备驱动(白嫖小知识~)
介绍数据包收包过程,有助于我们了解Linux内核网络设备在数据收包过程中的位置。数据包从被网卡接收到进入socket接收队列的整个过程,首先涉及网络设备初始化。以Intel I网卡的驱动ibg为例,驱动会在加载时调用初始化函数。pci_register_driver函数用于将驱动的各种回调方法注册到一个struct pci_driver变量中。通过PCI ID识别设备后,内核为设备选择合适的驱动。许多驱动需要大量代码使得设备就绪,如设置net_device_ops变量,注册ethtool函数,以及配置软中断。
网络设备启动过程中,业务源码是啥igb_probe函数完成设备初始化工作,包括PCI相关的操作和通用网络功能。结构net_device_ops变量包含了网络设备相关的操作函数,例如开启网络设备(ndo_open)时会调用对应的方法。在使用DMA将数据直接写入内存后,实现这一功能的数据结构为ring buffer。预留内存区域给网卡使用,实现数据包的接收。网卡支持接收侧扩展(RSS)或多队列技术,以利用多个CPU并行处理数据包。Intel I网卡支持多队列,其驱动在启用时调用igb_setup_all_rx_resources函数管理DMA内存。
启用NAPI(New API)接收数据包,通过调用napi_enable函数设置NAPI变量中的启用标志位。对于igb驱动,当网卡被启用或通过ethtool修改队列数量或大小时,会启用每个q_vector的NAPI变量。注册中断处理函数,不同驱动实现因硬件而异,一般优先考虑MSI-X中断方式,以实现更高效的数据处理。最后,打开硬中断,网卡便可以接收数据包。
监控网络设备有多种方式,从最粗粒度的ethtool -S查看网卡统计信息,到sysfs中获取接收端数据包的详细类型统计。sysfs提供统计信息,但驱动决定何时以及如何更新这些计数。/proc/net/dev提供了更高层级的网卡统计,适合作为常规参考。如果对数据的准确度有高要求,必须查看内核源码、驱动源码和驱动手册,以完全理解每个字段的实际含义。
本文仅介绍了Linux内核网络设备驱动的基本概述,未来将深入探讨更详细的内容,欢迎关注后续文章。
Linux 系统内核概述
Linux内核是一种开源的类Unix操作系统宏内核。
它是Linux操作系统的核心组件,同时也是计算机硬件与进程之间的桥梁。内核负责处理两者之间的通信,并高效地管理资源。内核被称为内核,是因为它在操作系统中扮演着类似种子在果实硬壳中的角色,掌控着硬件的主要功能。内核的主要用途包括以下四项工作:
在正确实施的情况下,内核对用户来说是不可见的,它在自己的小世界中(称为内核空间)工作,分配内存并跟踪内容的存储位置。用户所看到的内容被称为用户空间。这些应用通过系统调用接口(SCI)与内核进行交互。
1. 内核简介
Linux内核采用单内核体系设计,同时借鉴了微内核设计体系的优点,引入了模块化机制。
2. 内核模块
2.1 uname命令
使用格式:uname [选项]
参数解释:[选项]用于指定命令的功能,如-n显示内核名称。
2.2 lsmod命令
显示由核心已经装载的内核模块。
命令定义:lsmod [-v] [-c] [-s] [-m]
字段含义:[-v]显示详细模式,[-c]显示模块数量,[-s]显示模块大小,[-m]显示模块名称。
2.3 modinfo命令
显示模块的详细描述信息。
命令定义:modinfo [模块名称]
语法:modinfo [-v] [模块名称]
选项:[-v]显示详细模式。
2.4 modprobe命令
装载或卸载内核模块。
命令定义:modprobe [模块名称] [选项]
语法:modprobe [模块名称] [选项]
选项:[模块名称]指定要装载或卸载的模块。
2.5 depmod命令
内核模块依赖关系文件及系统信息映射文件的生成工具。
语法:depmod [-a] [-F file] [-e] [-n] [-N] [-v]
参数:[-a]生成所有模块的依赖关系,[-F file]指定依赖关系文件,[-e]仅显示错误信息,[-n]不生成依赖关系,[-N]不生成映射文件,[-v]显示详细模式。
2.6 insmod和rmmod命令
装载或卸载内核模块。
insmod命令:insmod [模块名称] [选项]
rmmod命令:rmmod [模块名称] [选项]
3. /proc目录
内核将自己内部状态信息、统计信息以及可配置参数通过proc伪文件系统输出。
3.1 sysctl命令
语法格式:sysctl [-n] [-e] [-f file] [-p] [-a] [-r] [-w] [name [...]]
命令参数:[-n]不打印数值,[-e]退出时显示错误,[-f file]指定配置文件,[-p]打印所有配置,[-a]显示所有参数,[-r]读取配置,[-w]写入配置,[name [...]]指定要设置的参数。
3.2 修改配置文件
3.3 实战演示
4. /sys目录
sysfs伪文件系统,输出内核识别出的各硬件设备的相关属性信息,以及内核对硬件特性的设定信息。有些参数可以修改,用于调整硬件工作特性。
4.1 udev
4.2 ramdisk文件的制作
方法一:使用dd命令
方法二:使用mkinitramfs命令
4.3 查看ramdisk
5. 编译内核
5.1 前提准备
(1) 准备好开发环境
(2) 获取目标主机上硬件设备的相关信息
(3) 获取到目标主机系统功能的相关信息
(4) 获取内核源代码包
5.2 简易安装内核
简易安装:简单依据模板文件的制作内核
5.3 详解编译内核
(1) 配置内核选项
(2) 编译 - make [-j #]
链接:blog.csdn.net/daocaokaf...
字符设备中的几个函数分析
1.在内核中, dev_t 类型(在 <linux/types.h>中定义)用来持有设备编号 — 主次部分都包括.其中dev_t 是 位的量, 位用作主编号, 位用作次编号
1 #ifndef _LINUX_TYPES_H
2 #define _LINUX_TYPES_H
3
4 #include <asm/types.h>
5
6 #ifndef __ASSEMBLY__
7 #ifdef __KERNEL__
8
9 #define DECLARE_BITMAP(name,bits) /
unsigned long name[BITS_TO_LONGS(bits)]
#endif
#include <linux/posix_types.h>
#ifdef __KERNEL__
typedef __u __kernel_dev_t;
typedef __kernel_fd_set fd_set;
typedef __kernel_dev_t dev_t; //用来持有设备编号的主次部分
typedef __kernel_ino_t ino_t;
typedef __kernel_mode_t mode_t;
...
2.在 <linux/kdev_t.h>中的一套宏定义. 为获得一个 dev_t 的主或者次编号, 使用:
2.1设备编号的内部表示
MAJOR(dev_t dev);
MINOR(dev_t dev);
2.在有主次编号时, 需要将其转换为一个 dev_t, 可使用:
MKDEV(int major, int minor);
在linux/kdev_t.h中有下了内容
...
4 #define MINORBITS
5 #define MINORMASK ((1U << MINORBITS) - 1)
6
7 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
8 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
9 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))//高为表示主设备号,低位表示次设备号
...
3.分配和释放设备编号register_chrdev_region函数
下面摘自文件fs/char_dev.c内核源代码
/
*** register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count; //计算分配号范围中的最大值+=
dev_t n, next;
for (n = from; n < to; n = next) { /*每次申请个设备号*/
next = MKDEV(MAJOR(n)+1, 0);/*主设备号加一得到的设备号,次设备号为0*/
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:/*当一次分配失败的时候,释放所有已经分配到地设备号*/
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
这里, from是要分配的起始设备编号. from 的次编号部分常常是 0, 但是没有要求是那个效果. count是你请求的连续设备编号的总数. 注意, 如果count 太大, 要求的范围可能溢出到下一个次编号;但是只要要求的编号范围可用, 一切都仍然会正确工作. 最后, name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中.如同大部分内核函数, 如果分配成功进行, register_chrdev_region 的返回值是 0. 出错的情况下, 返回一个负的错误码, 不能存取请求的区域.
4.下面是char_device_struct结构体的信息
fs/char_dev.c
static struct char_device_struct {
struct char_device_struct *next; // 指向散列冲突链表中的下一个元素的指针
unsigned int major; // 主设备号
unsigned int baseminor; // 起始次设备号
int minorct; // 设备编号的范围大小
const char *name; // 处理该设备编号范围内的设备驱动的名称
struct file_operations *fops; // 没有使用
struct cdev *cdev; /* will die指向字符设备驱动程序描述符的指针*/
} *chrdevs[MAX_PROBE_HASH];
/
** Register a single major with a specified minor range.
*
* If major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If major > 0 this function will attempt to reserve the passed range of
* minors and will return zero on success.
*
* Returns a -ve errno on failure.
*/
/
*** 该函数主要是注册注册注册主设备号和次设备号
* major == 0此函数动态分配主设备号
* major > 0 则是申请分配指定的主设备号
* 返回0表示申请成功,返 回负数说明申请失败
*/
static struct char_device_struct
*__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{ /*以下处理char_device_struct变量的初始化和注册*/
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
//kzalloc()分配内存并且全部初始化为0,
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
//ENOMEM定义在include/asm-generic/error-base.h中,
// #define ENOMEM /* Out of memory */
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
/* temporary */
if (major == 0) { //下面动态申请主设备号
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i—) {
//ARRAY_SIZE是定义为ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
//#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
if (chrdevs[i] == NULL)
//chrdevs是内核中已经注册了的设备好设备的一个数组
break;
}
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
ret = major;//这里得到一个位使用的设备号
}
//下面四句是对已经申请到的设备数据结构进行填充
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;/*申请设备号的个数*/
strlcpy(cd->name, name, sizeof(cd->name));
/*以下部分将char_device_struct变量注册到内核*/
i = major_to_index(major);
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major || //chardevs[i]设备号大于主设备号
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) || //chardevs[i]主设备号等于主设备号,并且此设备号大于baseminor
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
//在字符设备数组中找到现在注册的设备
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
}
/*所申请的设备好号能够满足*/
cd->next = *cp;/*按照主设备号从小到大顺序排列*/
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
以上程序大体上分为两个步骤:
1.char_device_struct类型变量的分配以及初始化~行
2.将char_device_struct变量注册到内核,行页到行
1.char_device_struct类型变量的分配以及初始化
(1)首先,调用 kmalloc 分配一个 char_device_struct 变量cd。
检查返回值,进行错误处理。
(2)将分配的char_device_struct变量的内存区清零memset。
(3)获取chrdevs_lock读写锁,并且关闭中断,禁止内核抢占,write_lock_irq。
(4)如果传入的主设备号major不为0,跳转到第(7)步。
(5)这时,major为0,首先需要分配一个合适的主设备号。
将 i 赋值成 ARRAY_SIZE(chrdevs)-1,其中的 chrdevs 是包含有个char_device_struct *类型的数组,
然后递减 i 的值,直到在chrdevs数组中出现 NULL。当chrdevs数组中不存在空值的时候,
ret = -EBUSY; goto out;
(6)到达这里,就表明主设备号major已经有合法的值了,接着进行char_device_struct变量的初始化。
设置major, baseminor, minorct以及name。
2.将char_device_struct变量注册到内核
(7)将 i 赋值成 major_to_index(major)
将major对取余数,得到可以存放char_device_struct在chrdevs中的索引
(8)进入循环,在chrdevs[i]的链表中找到一个合适位置。
退出循环的条件:
(1)chrdevs[i]为空。
(2)chrdevs[i]的主设备号大于major。
(3)chrdevs[i]的主设备号等于major,但是次设备号大于等于baseminor。
注意:cp = &(*cp)->next,cp是char_device_struct **类型,(*cp)->next是一个char_device_struct
*类型,所以&(*cp)->next,就得到一个char_device_struct **,并且这时候由于是指针,所以
对cp赋值,就相当于对链表中的元素的next字段进行操作。
(9)进行冲突检查,因为退出循环的情况可能造成设备号冲突(产生交集)。
如果*cp不空,并且*cp的major与要申请的major相同,此时,如果(*cp)->baseminor < baseminor + minorct,
就会发生冲突,因为和已经分配了的设备号冲突了。出错就跳转到ret = -EBUSY; goto out;
()到这里,内核可以满足设备号的申请,将cd链接到链表中。
()释放chrdevs_lock读写锁,开中断,开内核抢占。
()返回加入链表的char_device_struct变量cd。
()out出错退出
a.释放chrdevs_lock读写锁,开中断,开内核抢占。
b.释放char_device_struct变量cd,kfree。
c.返回错误信息
下面程序出自fs/char_dev.c
动态申请设备号
...
/
*** alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
/* dev:
仅仅作为输出参数,成功分配后将保存已分配的第一个设备编号。
baseminor:
被请求的第一个次设备号,通常是0。
count:
所要分配的设备号的个数。
name:
和所分配的设备号范围相对应的设备名称。
b.返回值:
成功返回0,失败返回负的错误编码
*/
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
...
Linux内核源码解析---mount挂载原理
Linux磁盘挂载命令"mount -t xxx /dev/sdb1 abc/def/"的底层实现原理非常值得深入了解。从内核初始化的vfsmount开始说起。
内核初始化过程中,主要关注"main.c"中的vfs_caches_init函数,这个方法与mount紧密相连。接着,跟进"mnt_init"和"namespace.c",关键在于最后的三个函数,它们控制了挂载过程的实现。
在"mount.c"中,sysfs_fs_type结构中包含了获取超级块的函数指针,而"init_rootfs"则注册了rootfs类型的文件系统。挂载系统调用sys_mount中的dev_name, dir_name和type参数,分别对应设备名称、挂载目录和文件系统类型。
"do_mount"方法通过path_lookup收集挂载目录信息,创建nameidata结构,然后调用do_add_mount进行实际挂载。这个过程涉及do_kern_mount和graft_tree,尽管具体实现较为复杂,但核心在于创建vfsmount并将其与namespace关联。
在"graft_tree"中的判断逻辑中,vfsmount被创建并与其父mount和挂载目录的dentry建立关系。在"attach_mnt"方法中,新vfsmount与现有结构关联,设置挂载点和父vfsmount,最终形成挂载的概念,即为设备分配vfsmount,并将其与指定目录和vfsmount结合,成为vfs系统的一部分。
Linux驱动开发之字符设备驱动入门
Linux中的设备驱动可以分为三种类型:字符设备驱动、块设备驱动和网络设备驱动。字符设备驱动以字符流为基础进行数据通信,如LCD、键盘、I2C等,特点是通信速度快且数据量小;块设备主要指存储设备,如磁盘、U盘、Flash、SD卡等,数据通信基于块;网络设备如以太网、WIFI,通信基于协议,通常是Socket协议。字符设备驱动允许用户通过文件I/O接口与设备通信,如open()、read()、write()、close()等。每一个驱动程序都必须拥有设备号,用于系统识别。为了开发字符设备驱动,需要申请设备号,参数包括主设备号、描述设备信息和文件操作对象。设备号的申请与释放函数分别为sysfs和devfs接口。创建设备节点可以通过手动或自动方式,手动创建需在控制台使用mknod命令,而自动创建则在驱动源码中调用接口函数实现。为了响应用户空间的文件I/O操作,需要在驱动源码中实现open()、read()、write()和close()四个函数。用户通过应用程序调用这些接口与驱动交互,实现与设备的通信。在驱动程序中,将数据从用户空间转换到内核空间,通常使用copy_from_user()和copy_to_user()函数。控制硬件设备通常需要通过虚拟地址进行,使用ioremap()函数将硬件物理地址映射到虚拟地址。最后,字符设备驱动的开发流程包括搭建加载和卸载函数框架、申请设备号、创建设备节点、初始化硬件、映射地址、响应文件操作和控制硬件设备。在操作寄存器地址时,可以使用物理地址映射成虚拟地址后直接赋值、通过readl()与writel()函数或使用ioremap()和iounmap()函数进行读写操作。