【一文了解】Linux文件系统与设备文件

【一文了解】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文件系统目录结构

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 等)。


  • 在设备驱动程序的设计中,一般而言,会关心 fileinode 这两个结构体。

(1)file结构体
file结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的 struct file 。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file 的指针通常被命名为 filefilp(即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 等目录下,会有 driversdevices 两个子目录,其中 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_typematch() 函数匹配并捆绑在一起,配对成功后就会调用对应驱动的 probe() 函数(比如 platform_driver.probe()、pci_driver.probe() 等)。

四、总结

Linux用户空间的文件编程有两种方法,即通过Linux API和通过C库函数访问文件。用户空间看不到
设备驱动,能看到的只有与设备对应的文件,因此文件编程也就是用户空间的设备编程。

Linux按照功能对文件系统的目录结构进行了良好的规划。Linux 2.6以后的内核通过一系列数据结构定义了设备模型,设备模型与sysfs文件系统中的目录和文件存在一种对应关系。设备和驱动分离,并通过总线进行匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值