整理文件系统基本操作。
此节只列出 Linux 和 c 库操作接口,具体的详细信息还是要找
man
。
需要注意的是:标准的ISOC库的I/O操作默认是带有缓存的,也就是填充一定的缓存后才会去调用系统接口。 而如果直接使用POSIX标准的系统接口,相当于上层没有做缓存,但实际上 内核为了尽量批量化的操作I/O,其内部也会做缓存。
1 | /** |
mode value | 含义 |
---|---|
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 |
1 | FILE *fopen( const char *filename, const char *mode ); |
1 | /** |
flags value | 含义 |
---|---|
O_RDONLY | 只读方式打开 |
O_WRONLY | 只写方式打开 |
O_RDWR | 读写方式打开 |
O_APPEND | 追加方式打开 |
O_CREAT | 创建 |
O_EXCL | 如果使用了O_CREATE且文件存在,就会发生错误 |
O_NOBLOCK | 以非阻塞的方式打开 |
O_TRUNC | 如果文件存在则删除其内容 |
... | ... |
STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO
位于头文件
<unistd.h>
中进行 read 和 write 大量数据读写时,需要考虑单次读写的字节数,取文件系统的block大小(比如4096字节),能在尽量减小系统调用的同时保证较高的写入效率。
1 | ssize_t read(int fd, void *buf, size_t count); |
1 | size_t fread( void *buffer, size_t size, size_t count, |
1 | /** |
whence value | 含义 |
---|---|
SEEK_SET | 文件开头 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件尾 |
1 | int fseek( FILE *stream, long offset, int origin ); |
1 | /** |
1 | int close(int fd); |
1 | int fclose( FILE *stream ); |
1 | int mkdir(const char *pathname, mode_t mode); |
1 | int rmdir(const char *pathname); |
1 | /*! |
多个进程打开同一个文件时,每个进程的 task_struct
都会包含此文件的资源描述,但是最终它们都是指向同一个 inode
。
lseek
这种操作时,如果没有造成文件的扩大,其实是直接操作的资源描述结构体,而没有去操作inode。如果有多个进程在操作同一个文件,则很有可能会造成竞态,有以下方式来避免此问题的发生:
1 | /** |
使用以下函数可以完成文件索引的复制(也就是两个不同的索引指向同一个文件描述资源,它们具有联动的偏移位置)
1 | int dup(int oldfd); |
一般的文件读写数据都会被存在 page cache 中,待内核在合适的时间写入硬盘,为了强制同步,可以使用下面函数:
1 | /** |
当一个文件已经打开,要修改它的一些属性时,可以使用函数
fcntl
。
1 | int fcntl(int fd, int cmd, ... /* arg */ ); |
此函数具有以下用途:
需要注意的是:
当要修改某个文件状态时,应该像操作寄存器位那样通过
读-修改-写
的方式操作(也就是先读取当前设置值,然后写入新设置的那一位,再回写回去)。
1 |
|
另外的一个控制函数便是 ioctl
,这个在驱动的操作中经常使用:
1 | int ioctl(int fd, unsigned long request, ...); |
平时使用最多的 shell 命令 ls -al
就是提取的文件属性来显示。
1 | struct stat { |
如下代码所示,使用 lstat 来判断文件类型:
1 |
|
与操作文件相关的 ID 具有下面几类:
类型 | 说明 |
---|---|
真实用户ID和真实组ID | 表示当前是哪个用户位于哪个组正在访问此文件 |
有效用户ID,有效组ID和补充组ID | 表示该文件允许的用户和组(在没有suid,sgid的情况下,此值与真实用户和真实组ID是一个值) |
suid | 当文件user的可执行权限打开并设置了suid后,其他用户可以以该文件所有者的权限来运行此文件 |
sgid | 当文件group的可执行权限打开并设置了sgid后,其他用户可以以该文件组成员的权限来运行此文件 |
rwx
不得不提的是:
x
权限,要读取目录内容列表信息,至少要具有 rx
权限。rw
权限。
可以使用下面的函数来判断当前进程是否有权限访问某个文件:
1 | int access(const char *pathname, int mode); |
1 | //修改文件权限 |
每增加一个硬链接,文件的链接数量加1,以表示有多少个文件引用到同一个inode.
1 | int link(const char *oldpath, const char *newpath); |
每取消一个硬链接,文件的链接数量减1,当一个文件的链接数量减至0 并且没有进程打开此文件时 ,文件既被删除。
1 | int unlink(const char *pathname); |
1 | int symlink(const char *target, const char *linkpath); |
1 | ssize_t readlink(const char *pathname, char *buf, size_t bufsiz); |
1 | int rename(const char *oldpath, const char *newpath); |
在设备驱动中,会关心 file 和 inode 这两个结构体。
f_flags
private_data
保存该设备驱动申请的数据地址1 | struct file { |
1 | /* |
i_rdev
表示设备编号,由高12位主设备号和低20位次设备号组成,使用下面的函数获取主次设备号
1 |
|
/proc/devices
中得到注册设备的主设备号和设备名1 | cat /proc/devices |
/dev/
下得到注册设备的主次设备号1 | Linux设计中强调的一个基本观点是机制和策略分离。 |
udev完全在用户态工作,利用设备加入或移出时内核所发送的热拔插事件(Hotplug Event)来工作。 在热拔插时,设备的详细信息会由内核通过netlink套接字发送出来,发出的事件叫uevent。 udev的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用从内核收到的信息来进行创建设备文件节点等工作。
udev的工作过程:
使用下面的代码就可以接收 netlink 消息:
1 |
|
如果想要让内核主动发出一次 uevent,则可以对 /sys/module
中的模块主动写 add
命令:
1 | echo add > /sys/module/psmouse/uevent |
会输出类似以下的消息:
1 | add@/module/psmouse |
sysfs是内核设备模型的一个全局概览,此目录下的多个顶层文件是站在不同的角度来查看设备模型的:
bus
是以总线的视角来看待。
devices,drivers
文件夹
/sys/devices
中文件的符号链接devices
是以设备的视角看待
driver
目录class
是以设备种类的视角看待设备
/sys/devices
中文件的符号链接block
是单独列出块设备文件dev
是块设备和字符设备文件在代码实现中,分别使用 bus_type,device_driver,device
来描述总线、驱动和设备:
1 | struct bus_type { |
1 | struct device_driver { |
1 | struct device { |
device_driver 和 device 都依附于总线,所以都包含了
bus_type
指针。而 device 又由 driver 驱动,所以它还包含了
device_driver
指针。
设备和驱动都是分开被注册的,总线的match
函数来进行对应的匹配,匹配成功后驱动的probe()
函数就会被调用。
总线、设备和驱动都会映射在 sysfs
中,其中的目录来源于
bus_type,device_driver,device
,而目录中的文件来源于
attribute
。
1 | struct attribute { |
对于以上结构,内核提供了快捷的操作宏:
1 |