【一文了解】Linux文件系统与设备文件
PS:第一章都是讲的Linux文件操作API,包括Linux系统调用以及标准C库。对此有了解的或者仅想了解Linux文件系统和设备文件的可直接跳到第二章。
文章目录
前言
“一切都是文件”是Linux系统的核心设计思想,掌握Linux文件系统、设备文件系统的知识就显得相当重要了。
从应用层的角度看,驱动最终是通过与文件操作相关的系统调用,或者基于这些系统调用的 C 库函数来被访问的;因此,设备驱动的设计本质上是为了契合应用程序所使用的 API 接口。另一方面,对驱动开发者来说,设备文件系统几乎是绕不开的环节。从 Linux 2.4 内核中引入的 devfs,到 Linux 2.6 以后逐步取代它的 udev,驱动工程师都需要与这些机制打交道,以便正确管理和使用设备节点。
一、Linux文件操作
1.1 文件操作的Linux系统调用
Linux的文件操作系统调用(在Windows编程领域,习惯称操作系统提供的接口为API)涉及 创建creat()、打开open()、读写read()write()和关闭文件close()。
- 1.创建creat()
int creat(const char *filename, mode_t mode);
参数mode指定新建文件的存取权限,它同umask一起决定文件的最终权限(mode&umask),其中,umask代表了文件在创建时需要去掉的一些存取权限。umask可通过系统调用 umask() 来改变。
int umask(int newmask);
该调用将umask设置为newmask,然后返回旧的umask,它只影响读、写和执行权限。
(1)打开open()
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open() 函数有两个形式,其中 pathname 是我们要打开的文件名(包含路径名称,缺省是认为在当前路径下面),flags 可以是下表中的一个值或者是几个值的组合。
| 标志 | 含义 |
|---|---|
| O_RDONLY | 以只读的方式打开文件 |
| O_WRONLY | 以只写的方式打开文件 |
| O_RDWR | 以读写的方式打开文件 |
| O_APPEND | 以追加的方式打开文件 |
| O_CREAT | 创建一个文件 |
| O_EXEC | 如果使用了O_CREAT而且文件已经存在,就会发生一个错误 |
| O_NOBLOCK | 以非阻塞的方式打开一个文件 |
| O_TRUNC | 如果文件已经存在,则删除文件的内容 |
O_RDONLY、O_WRONLY、O_RDWR 三个标志只能使用任意的一个。
如果使用了O_CREAT标志,则使用的函数是int open(const char*pathname,int flags,mode_t mode);这个时候我们还要指定mode标志,以表示文件的访问权限。mode可以是下表中所列值的组合。
| 标志 | 含义 |
|---|---|
| S_IRUSR | 用户可以读 |
| S_IWUSR | 用户可以写 |
| S_IXUSR | 用户可以执行 |
| S_IRWXU | 用户可以读、写、执行 |
| S_IRGRP | 组可以读 |
| S_IWGRP | 组可以写 |
| S_IXGRP | 组可以执行 |
| S_IRWXG | 组可以读、写、执行 |
| S_IROTH | 其他人可以读 |
| S_IWOTH | 其他人可以写 |
| S_IXOTH | 其他人可以执行 |
| S_IRWXO | 其他人可以读、写、执行 |
| S_ISUID | 设置用户执行ID |
| S_ISGID | 设置组的执行ID |
除了通过宏(如 O_CREAT、O_RDWR 等)组合标志来控制文件打开方式外,Linux 还允许我们用一个 八进制数字 来表示文件的权限。
这个数字由五位组成,从左到右依次代表:
第一位: 设置 用户 ID(setuid)位
第二位: 设置 组 ID(setgid)位
第三位: 文件所有者的权限
第四位: 所属用户组的权限
第五位: 其他用户的权限
在权限位中,每一位可以取以下值的组合:
1 —— 执行权限(x)
2 —— 写权限(w)
4 —— 读权限(r)
0 —— 无权限
通过将这些值相加,就能表示不同的权限组合。例如:
7 = 4 + 2 + 1 表示可读、可写、可执行(rwx)
5 = 4 + 1 表示可读、可执行(r-x)
举个例子:
如果我们希望创建这样一个文件:
用户(owner):可读、可写、可执行
组(group):没有任何权限
其他用户(others):可读、可执行
并且设置用户 ID 位
则对应的模式是:
1(设置用户ID) 0(不设组ID) 7(用户r-w-x) 0(组 无权限) 5(其他 r-x)
合在一起就是:10705
上述等价于:
open("test", O_CREAT, 10705);
或
open("test", O_CREAT, S_IRWXU | S_IROTH | S_IXOTH | S_ISUID );
如果文件打开成功,open函数会返回一个文件描述符,以后对该文件的所有操作就可以通过对这个文
件描述符进行操作来实现
(2)读写read()、write()
在文件打开以后,我们才可对文件进行读写,Linux中提供文件读写的系统调用是read、write函数:
int read(int fd, const void *buf, size_t length);
int write(int fd, const void *buf, size_t length);
其中,参数 buf 为指向缓冲区的指针, length 为缓冲区的大小(以字节为单位)。函数read()实现从文件描述符fd所指定的文件中读取 length 个字节到 buf 所指向的缓冲区中,返回值为实际读取的字节数。函数write()实现把length个字节从buf指向的缓冲区中写到文件描述符fd所指向的文件中,返回值为实际写入的字节数。
以 O_CREAT 为标志的open实际上实现了文件创建的功能,因此,下面的函数等同于creat()函数:
int open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);
(3)定位lseek()
对于随机文件,我们可以随机指定位置进行读写,使用如下函数进行定位:
int lseek(int fd, offset_t offset, int whence);
lseek()将文件读写指针相对whence移动offset个字节。操作成功时,返回文件指针相对于文件头的
位置。参数whence可使用下述值:
SEEK_SET: 相对文件开头
SEEK_CUR: 相对文件读写指针的当前位置
SEEK_END: 相对文件末尾
offset 可取负值,表示文件指针相对于当前位置向前移动
由于lseek函数的返回值为文件指针相对于文件头的位置,因此下列调用的返回值就是文件的长度:
lseek(fd, 0, SEEK_END);
(4)关闭close()
当我们操作完成以后,要关闭文件,此时,只要调用close就可以了,其中fd是我们要关闭的文件描述
符:
int close(int fd);
(5)历程
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#define LENGTH 100
main()
{
int fd, len;
char str[LENGTH];
fd = open("hello.txt", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); /*创建并打开文件 */
if (fd) {
write(fd,"Hello World", strlen("Hello World")); /* 写入字符串 */
close(fd);
}
fd = open("hello.txt", O_RDWR);
len = read(fd, str, LENGTH); /* 读取文件内容 */
str[len] = '\0';
printf("%s\n", str);
close(fd);
}
编译并运行,执行结果为输出“Hello World”
1.2 C库文件操作
C库函数的文件操作实际上独立于具体的操作系统平台,不管是在DOS、Windows、Linux还是在
VxWorks中都是这些函数:
(1) 创建和打开
fiLE *fopen(const char *path, const char *mode);
**fopen()**用于打开指定文件 filename,其中的mode为打开模式,C库函数中支持的打开模式如下表所示。
| 标志 | 含义 |
|---|---|
| r、rb | 以只读方式打开 |
| w、wb | 以只写方式打开。如果文件不存在,则创建该文件,否则文件被截断 |
| a、ab | 以追加方式打开。如果文件不存在,则创建该文件 |
| r+、r+b、rb+ | 以读写方式打开 |
| w+、w+b、wh+ | 以读写方式打开。如果文件不存在时,创建新文件,否则文件被截断 |
| a+、a+b、ab+ | 以读和追加方式打开。如果文件不存在,则创建新文件 |
其中,b 用于区分二进制文件和文本文件,这一点在DOS、Windows系统中是有区分的,但Linux不区分二进制文件和文本文件。
(2)读写
C库函数支持以字符、字符串等为单位,支持按照某种格式进行文件的读写,这一组函数为:
int fgetc(fiLE *stream);
int fputc(int c, fiLE *stream);
char *fgets(char *s, int n, fiLE *stream);
int fputs(const char *s, fiLE *stream);
int fprintf(fiLE *stream, const char *format, ...);
int fscanf (fiLE *stream, const char *format, ...);
size_t fread(void *ptr, size_t size, size_t n, fiLE *stream);
size_t fwrite (const void *ptr, size_t size, size_t n, fiLE *stream);
fread() 实现从流 (stream) 中读取 n 个字段,每个字段为 size 字节,并将读取的字段放入 ptr 所指的字符数组中,返回实际已读取的字段数。当读取的字段数小于 num 时,可能是在函数调用时出现了错误,也可能是读到了文件的结尾。因此要通过调用 feof() 和 ferror() 来判断。
**write()**实现从缓冲区ptr所指的数组中把n个字段写到流(stream)中,每个字段长为size个字节,返
回实际写入的字段数。
另外,C库函数还提供了读写过程中的定位能力,这些函数包括
int fgetpos(fiLE *stream, fpos_t *pos);
int fsetpos(fiLE *stream, const fpos_t *pos);
int fseek(fiLE *stream, long offset, int whence);
(3)关闭
利用C库函数关闭文件依然是很简单的操作:
int fclose (fiLE *stream);
(4)历程
#include <stdio.h>
#define LENGTH 100
main ()
{
fiLE *fd;
char str[LENGTH];
fd=fopen("hello.txt","w+");/*创建并打开文件*/
if (fd){
fputs("Hello World",fd);/*写入字符串*/
fclose(fd);
}
fd =fopen("hello.txt","r");
fgets(str,LENGTH,fd); /*读取文件内容*/
printf("s\n",str);
fclose(fd);
}
二、Linux文件系统
2.1 Linux文件系统目录结构
- 对于这一节,可以直接看一下这个文章
linux系统下各个文件的作用
2.2 Linux文件系统与设备驱动
如图所示为Linux中虚拟文件系统、磁盘/Flash文件系统及一般的设备文件与设备驱动程序之间的关
系。

应用程序和VFS之间的接口是系统调用,而VFS与文件系统以及设备文件之间的接口是file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数,关系如下图所示

由于字符设备没有文件系统,应用层对它的操作直接由驱动程序提供的 file_operations 来处理,所以 file_operations 是字符设备驱动的核心。(详见,Linux下字符设备驱动)
块设备有两种访问方式:
1.直接访问裸设备:用统一的 def_blk_fops(比如 dd if=/dev/sdb1 of=sdb1.img 复制整个分区)。
2.通过文件系统访问:文件系统自己实现 file_operations,把文件的读写请求转换成块设备的读写操作(如 ext2、fat、btrfs 等)。
- 在设备驱动程序的设计中,一般而言,会关心 file 和 inode 这两个结构体。
(1)file结构体
file结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的 struct file 。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file 的指针通常被命名为 file 或 filp(即file pointer)。
- file文件结构体的定义
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations*f_op; /* 和文件关联的操作*/
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags; /*文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC*/
fmode_t f_mode; /*文件读/写模式,FMODE_READ和FMODE_WRITE*/
struct mutex f_pos_lock;
loff_t f_pos; /* 当前读写位置 */
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_statef_ra;
u64 f_version;
#ifdef CONfiG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data; /*文件私有数据*/
#ifdef CONfiG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONfiG_EPOLL */
struct address_space*f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
(2) inode结构体
VFS inode包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是
Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁,inode结构体的定义如下所示:
struct inode {
...
umode_t i_mode; /* inode的权限 */
uid_t i_uid; /* inode拥有者的id */
gid_t i_gid; /* inode所属的群组id */
dev_t i_rdev; /* 若是设备文件,此字段将记录设备的设备号 */
loff_t i_size; /* inode所代表的文件大小 */
struct timespec i_atime; /* inode最近一次的存取时间 */
struct timespec i_mtime; /* inode最近一次的修改时间 */
struct timespec i_ctime; /* inode的产生时间 */
unsigned int i_blkbits;
blkcnt_t i_blocks; /* inode所使用的block数,一个block为512 字节 */
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
/* 若是块设备,为其对应的block_device结构体指针 */
struct cdev *i_cdev; /* 若是字符设备,为其对应的cdev结构体指针 */
}
...
};
对于表示设备文件的inode结构,i_rdev 字段包含设备编号。Linux内核设备编号分为主设备编号和次设备编号,前者为 dev_t 的高12位,后者为 dev_t 的低20位。下列操作用于从一个inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
主设备号是与驱动对应的概念,同一类设备一般使用相同的主设备号,不同类的设备一般使用不同的
主设备号(但是也不排除在同一主设备号下包含有一定差异的设备)。因为同一驱动可支持多个同类设
备,因此用次设备号来描述使用该驱动的设备的序号,序号一般从0开始。
三、sysfs文件系统与Linux设备模型
从 Linux 2.6 开始,引入了 sysfs 文件系统。它是一个虚拟文件系统,用来以分层目录的方式展示系统里的所有硬件信息,作用类似于 proc 文件系统(proc 用来展示进程和系统状态)。
sysfs把连接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间
导出内核数据结构以及它们的属性。sysfs 的一个目的就是展示设备驱动模型中各组件的层次关系,其顶级目录包括block、bus、dev、devices、class、fs、kernel、power和firmware等。
在 /sys 目录下:
- block:保存所有块设备
- devices:保存系统所有设备,按总线层级组织
- bus:保存所有总线类型
- class:保存各种设备类型(如网卡、声卡、输入设备等)
用 tree /sys 命令可以看到一个很长的目录树(部分):

在 /sys/bus/pci 等目录下,会有 drivers 和 devices 两个子目录,其中 devices 里的文件其实是指向 /sys/devices 的符号链接。
同样,/sys/class 目录下也有很多指向 /sys/devices 的链接。
这样设计,使得 Linux 设备模型能清晰地对应设备、驱动、总线和设备类别。

随着硬件越来越复杂,对电源管理、热插拔和即插即用的要求也更高,所以从 Linux 2.6 开始,引入了新的设备模型,把 设备、总线、类和驱动 有机地组织在一起,更好地管理系统。下图形象的展示了Linux驱动模型中设备、总线和类之间的关系。

在 Linux 2.6 以后,设备模型的核心关系由内核自动处理,总线和子系统会负责和设备模型交互。驱动工程师写驱动时不用关心这些细节,只要按照框架要求,在 xxx_driver 里填好对应的回调函数就行(xxx 就是总线名)。
在 Linux 内核里,device 表示设备,device_driver 表示驱动,它们都依赖某种总线,所以都带有 bus_type 指针。
设备和驱动是分开注册的,互相不存在先后顺序。它们进入内核后,会通过 bus_type 的 match() 函数匹配并捆绑在一起,配对成功后就会调用对应驱动的 probe() 函数(比如 platform_driver.probe()、pci_driver.probe() 等)。
四、总结
Linux用户空间的文件编程有两种方法,即通过Linux API和通过C库函数访问文件。用户空间看不到
设备驱动,能看到的只有与设备对应的文件,因此文件编程也就是用户空间的设备编程。
Linux按照功能对文件系统的目录结构进行了良好的规划。Linux 2.6以后的内核通过一系列数据结构定义了设备模型,设备模型与sysfs文件系统中的目录和文件存在一种对应关系。设备和驱动分离,并通过总线进行匹配。

3164

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



