Linux应用 文件I/O
Linux系统下文件的存储
Linux系统下文件的存储是由两部分存储的,第一部分是存储文件的具体数据,第二部分是inode表,inode 是一个描述文件的结构体,其描述了文件的基本信息和存储位置,通过找到inode的编号就可以找到对应的inode,通过inode就可以找到其存储信息,进而完成对文件的读写。
ls -i //查看当前目录下文件的inode标号
stat file_path //查看指定文件的inode标号
tips:格式化有时候分为格式化和快速格式化,快速格式化只是删除了磁盘的inode表,所以快速格式化的u盘是可以找回数据的(在原来的存储信息没有被覆盖之前)。
打开一个文件的步骤
1、系统通过文件名称找到对应的inode编号
2、通过inode编号在inode表中找到对应的inode结构体
3、通过inode结构体,找到对应的存储block信息,然后在磁盘中读取文件。
文件在被读取以后,系统会malloc一块内存区域,文件会被存在内存中进行管理、缓存,我们进行操作的话是对内存中的文件进行操作,不是针对磁盘中存放的静态文件。
为什么Linux系统要把文件读取内存中然后进行读写操作呢
1、内存的访问速度是大于外部磁盘的,把文件读取到内存中,能够有效的提高速率
2、Linux对于存储设备的访问是以块为单位的,对内存的访问是以字节为单位的,所以在内存中操作文件比较灵活。
PCB(进程控制块)中有一个指针指向文件描述表,文件描述表中的每一个元素索引代表一个文件表,文件表记录一个文件的相关信息,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都指向一个对应的文件表,当前程对于文件的操作都是基于这个文件对应的文件表的。
返回错误
Linux系统下对于常见的错误都做了一个编号。
每一个进程都有一个自己维护的Errno变量,是程序中的全局变量,用来存储就近发生的函数执行错误编号。
如何获取系统所维护的Errno变量呢-------->>在文件加上头文件#include <errno.h> ,可以认为这个变量是在这个头文件中声明的。
如何解析这个errno变量呢--------->> 使用c库函数 strerror() 将对应的 errno 转换成适合我们查看的字符串信息
#include <string.h>
char* strerror(int errnum);
有没有直接根据errno值打印对应错误的函数呢-------->>
#include <stdio.h>
void perror(const char*s) //可以在输出的错误提示字符串之前加入自己的打印信息 也就是%s可以自己传入,然后会在错误之前加上冒号和空格打印。
退出
_exit()和__Exit()
调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程中所有文件描述符,结束进程,移交控制权给操作系统,其实就
_exit() 和__Exit()等价
#include <unistd.h>
void _exit(int status) //传入0表示正常结束,其他值表示有错误发生,系统调用#include <stdlib.h>
void exit(int status); //标准c库函数
空洞文件
空洞文件指的是,在还没有使用到这个空间的时候,提前开辟出这个文件空间的大小,没有存数据的那部分就属于是空洞文件。
空洞文件的作用:1、方便多线程同时下载文件
2、虚拟机分配内存空间。
ls查看文件的逻辑大小
du命令查看空洞文件
使用read函数读取文件空洞部分会读到什么呢?
在 Linux 上只要文件系统支持“空洞”(hole,即 sparse file),用 read() 去读空洞时,内核会返回全 0 字节(\0),行为与读普通全-0 区域完全一样;既不会报错,也不会缩短返回长度(除非读到 EOF)
O_TRUNC
在打开文件以后自动将文件清空
O_APPEND
打开文件后自动将写指针指向文件末尾,但不会影响读指针,读指针依旧指向文件开头。
tips
O_TRUNC的优先级大于O_APPEND
多次打开同一个文件
区别在于是否会生成新的文件表
1、如果多次打开同一个文件的话,会得到不同的文件修饰符,也就是open一次就会得到一个文件修饰符, 因为文件修饰符是有限资源,所以需要自己手动释放也就是使用close关闭文件,一个文件描述符对应一个独立的文件表,但是这个文件表指向的是同一个inode和同一个动态文件。
2、前面说过,在我们打开文件的时候,操作系统会将文件读入内存中,生成文件的动态文件,那么当我们多次打开同一个文件的时候,操作系统会生成多个动态文件吗?答案是不会的,当我们多次打开一个文件时,系统只会在内存中生成一个动态文件。
3、既然系统只在内存中生成一个动态文件,那么对于多个文件修饰符,程序中对文件的读写操作是同时操作一个文件还是分别操作。答案是分别操作,不同的文件描述符对应的读写指针是独立的。
4、如果打开多个文件的时候FLAG标志都有O_APPEND,那么写指针会一直指在文件尾部,也就是多个文件描述符的写操作都是从文件末尾开始的。
文件描述符的复制
Linux系统中,open获得的文件描述符可以被复制,复制得到的文件描述符,权限与原文件描述符相同。这些文件描述符指向一个文件表。
#include <unistd.h>
int dup(int oldfd) //复制文件描述符的系统调用 新编号由系统指定
int dup2(int oldfd,int newfd) //复制文件描述符,可指定新编号
复制得到的文件描述符指向同一个文件表。所以复制来的文件描述符,所以读写指针是相同的。
文件共享
文件共享指的就是多个不同的文件描述符指向同一个inode节点。
同一个进程中使用Open打开同一个文件。
不同文件描述符指向不同文件表,不同文件表指向同一个inode节点和动态文件。
)
这个时候,不同的文件描述符对应着对这个文件的不同操作,是分别进行的。
不同的进程中open打开同一个文件
不同文件描述表中的不同文件描述符,指向不同文件描述表,指向同一个inode节点和动态文件。
和上面一个,指向同一个inode节点,读写操作是分别进行的。
同一个进程中使用dup复制文件描述符
不同文件描述符,指向同一个文件表,指向同一个inode节点和动态文件。
不同的文件描述符指向同一个节点,对于文件的读写操作是共享的。
原子操作与竞争冒险
竞争冒险出现在两个不同的进程操作同一个文件时,因为两个文件对于这个文件的操作是独立的(不使用O_APPEND flag).所以在同时对文件操作时,会出现其中一方的操作被覆盖,进而导致数据丢失。
原子操作就是将可能会导致竞争冒险的操作原子化,也就是本来多个步骤的操作可能会因为任务切换而导致出错,现在保证这个操作能一气呵成,中间不被打断,这样的操作就叫原子操作。原子操作要么不执行,要执行就要一口气执行完。
O_APPEND标志就会在每次write的时候,将文件的写指针移动到文件末尾,然后进行write,然后在写入数据。这个操作同时也就组成了原子操作。这里的移动指针和写数据是一气呵成的
pread()和Pwrite()用法和功能和平常的read()和write()相同,但是他们是原子操作。
#include <unistd.h>
ssize_t pread(int fd,void* buf,size_t count,off_t offset)
ssize_t pwrite(int fd,void* buf,size_t count,off_t offset)
//支持指定当前读写的偏移量,但是两个操作不会更改文件的读写指针位置
O_EXCL标志保证了,检查文件是否存在,不存在就创建的原子操作。检查和创建是一气呵成的
fcntl和ioctl
fcntl是操作文件描述符的。
fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。fcntl()函数原型如下所示(可通过"man 2 fcntl"命令查看)
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd,int cmd , .../*arg*/) ;
//1、复制文件标识符cmd=F_DUPFD
//2、获取设置文件描述符标志 cmd=F_GETFD 或 cmd=F_SETFD 只有 O_APPEND O_ASYNC O_DIRECT O_NOATIME O_NONBLOCK可以修改
ioctl是发送和接受命令的,一般都用于接受。
ioctl()可以认为是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设,譬如可以通过 ioctl 获取 LCD 相关信息等,
#include <sys/ioctl.h>
int ioctl(int fd,unsigned long request,...); //给对应的fd文件发送request请求,然后在驱动文件中编写不同request对应不同的操作/*在LEDAPP应用文件中*/
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>#define LED_MAGIC 'L'
#define LED_ON _IO(LED_MAGIC, 0)
#define LED_OFF _IO(LED_MAGIC, 1)int main(int argc, char *argv[])
{int fd = open("/dev/myled0", O_RDWR);if (fd < 0) { perror("open"); return 1; }if (argc > 1 && argv[1][0] == '1')ioctl(fd, LED_ON); /* 亮 */elseioctl(fd, LED_OFF); /* 灭 */close(fd);return 0;
}/*在led驱动文件中*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/gpio/consumer.h>#define LED_MAGIC 'L'
#define LED_ON _IO(LED_MAGIC, 0)
#define LED_OFF _IO(LED_MAGIC, 1) //自定义命令类型static struct gpio_desc *led_gpio;static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{switch (cmd) {case LED_ON:gpiod_set_value(led_gpio, 1);return 0;case LED_OFF:gpiod_set_value(led_gpio, 0);return 0;default:return -ENOTTY;}
}static const struct file_operations led_fops = {.owner = THIS_MODULE,.unlocked_ioctl = led_ioctl,
};static int __init led_probe(struct platform_device *pdev)
{led_gpio = gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio);return register_chrdev(0, "myled", &led_fops); /* 0→动态主设备号 */
}static int __exit led_remove(struct platform_device *pdev)
{gpiod_put(led_gpio);unregister_chrdev(0, "myled");return 0;
}static const struct of_device_id led_of_match[] = {{ .compatible = "foo,myled" },{}
};
MODULE_DEVICE_TABLE(of, led_of_match);static struct platform_driver led_driver = {.probe = led_probe,.remove = __exit_p(led_remove),.driver = {.name = "myled",.of_match_table = led_of_match,},
};
module_platform_driver(led_driver);MODULE_LICENSE("GPL");
截断文件
两个函数 truncate()和ftruncate()
#include <unistd.h>
#include <sys/types.h>int truncate(const char* path,off_t length); //使用文件路径指定文件
int ftruncate(int fd,off_t length); //使用文件修饰符指定文件 文件必须是open得到的,且必须有可写权限
//两个文件都可以将指定文件截断成指定大小 多了就砍掉后面的,小了就扩展成那么大的,用'/0'填充
//截断文件不会改变读写指针位置
图片来源 正点原子,个人学习,侵权删