Linux_基础IO详解
✨✨ 欢迎大家来到小伞的大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:LInux_st
小伞的主页:xiaosan_blog制作不易!点个赞吧!!谢谢喵!!
1. 理解“文件”
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设(即是输出设备也是输入设备)
- 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出简称IO
在C语言中,我们知道键盘,显示器,磁盘都是文件,在Linux中也是如此,键盘、显示器、网卡、磁盘·…..这些都是抽象化的过程
1.1 文件操作的认识
- 当我们创建文件时,对于OKB的空文件是占用磁盘空间的,磁盘会保存文件的属性(创建时间,创建路径......)
- 文件是文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容)
- 所有的文件操作本质是文件内容操作和文件属性操作
2. 回顾C语言文件接口
2.1 打开文件
#include <stdio.h>
int main()
{FILE *fp = fopen("myfile", "w");if (!fp){printf("fopen error!\n");}while(1);//这里我们等待,查看进程fclose(fp);return 0;
}
cwd:指向当前进程运行目录的一个符号链接。
exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
2.2 写入文件
#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "w");if (!fp){printf("fopen error!\n");}const char *msg = "hello IO!\n";int count = 5;while (count--){fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0;
}
2.3 读取文件
#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "r");if (!fp){printf("fopen error!\n");return 1;}char buf[1024];const char *msg = "hello bit!\n";while (1){// 注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明size_t s = fread(buf, 1, strlen(msg), fp);if (s > 0){buf[s] = 0;printf("%s", buf);}if (feof(fp)){break;}}fclose(fp);return 0;
}
我们也能通过读取文件实现cat指令
#include <stdio.h>
#include <string.h>int main(int argc, char *argv[])
{if (argc != 2)//cat+文件{printf("argv error!\n");return 1;}FILE *fp = fopen(argv[1], "r");if (!fp){printf("fopen error!\n");return 2;}char buf[1024];while (1){int s = fread(buf, 1, sizeof(buf), fp);if (s > 0){buf[s] = 0;printf("%s", buf);}if (feof(fp)){break;}}fclose(fp);return 0;
}
2.4 输出到显示器
int main()
{const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}
2.5 stdin & stdout & stderr
- 在C语言中,会默认打开三个输出流,stdin & stdout & stderr
- 仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,文件指针
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
标准输入stdin:0001
标准输出stdout:0010
标准错误stderr:0100
#include <stdio.h>
#define ONE 0001 // 0000 0001
#define TWO 0002 // 0000 0010
#define THREE 0004 // 0000 0100
void func(int flags)
{if (flags & ONE)printf("flags has ONE! ");if (flags & TWO)printf("flags has TWO! ");if (flags & THREE)printf("flags has THREE! ");printf("\n");
}
int main()
{func(ONE);func(THREE);func(ONE | TWO);func(ONE | THREE | TWO);return 0;
}
3. 系统调用-open接口
pathname:要打开或创建的目标文件
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags
参数:
O_RDONLY:只读打开
O_WRONLY:只写打开
O_RDWR:读,写打开
这三个常量,必须指定一个且只能指定一个O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND:追加写返回值:
成功:新打开的文件描述符
失败:-1
3.1 open写入
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{umask(0);//权限一般0002int fd = open("myfile", O_WRONLY | O_CREAT, 0644);if (fd < 0){perror("open");return 1;}int count = 5;const char *msg = "hello bit!\n";int len = strlen(msg);while (count--){write(fd, msg, len); // fd: 后⾯讲, msg:缓冲区⾸地址, len: 本次读取,期望写⼊多少个字节的数据。 返回值:实际写了多少字节数据}close(fd);return 0;
}
3.2 open读取
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{int fd = open("myfile", O_RDONLY);if (fd < 0){perror("open");return 1;}const char *msg = "hello IO!\n";char buf[1024];while (1){ssize_t s = read(fd, buf, strlen(msg)); // 类⽐writeif (s > 0){printf("%s", buf);}else{break;}}close(fd);return 0;
}
3.3 文件描述符fd
3.3.1 0&1&2
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2.
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{//close(0);//当我们没有关闭标识符0,返回3。//当我们关闭标识符0,返回0,//说明标识符的打开是跟随从小到大,最小的优先占据的规则int fd = open("myfile", O_RDONLY);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{//close(1);//当我们关闭标准输出,此时不会打印到屏幕上,而是由文件占据1这个文件标识符int fd = open("myfile", O_WRONLY | O_CREAT, 00644);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);//会向1文件标识符打印,此时就会向myfile文本打印fflush(stdout);close(fd);exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile当中,其中,fd=1。这
种现象叫做输出重定向。常见的重定向有:>,>>,<
//当我们关闭标准输出,此时不会打印到屏幕上,而是由文件占据1这个文件标识符
//会向1文件标识符打印,此时就会向myfile文本打印
3.4 使用dup2系统调用
#include <unistd.h>int dup2(int oldfd, int newfd);
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{//int fd = open("./log", O_RDWR | O_APPEND);//追加重定向int fd = open("./log", O_CREAT | O_RDWR);//输出重定向if (fd < 0){perror("open");return 1;}close(1);dup2(fd, 1);//此时1标识符关闭,fd的地址交给1,所以printf会向文本输出for (;;){char buf[1024] = {0};ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0){perror("read");break;}printf("%s", buf);fflush(stdout);}return 0;
}
4. 一切皆文件
在Linux中,一切皆文件(先描述,后组织),比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;
这样的好处很直接,,Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用
read函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE)的操作都可以用write函
数来进行。
struct file
{... struct inode *f_inode; /* cached value */const struct file_operations *f_op;... atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向它,就会增加f_count的值。 unsigned int f_flags; // 表⽰打开⽂件的权限fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义loff_t f_pos; // 表⽰当前读写⽂件的位置...} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
struct file_operations
{struct module *owner;// 指向拥有该模块的指针;loff_t (*llseek)(struct file *, loff_t, int);// llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);// ⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -EINVAL("Invalid argument")失败.⼀个⾮负返回值代表了成功读取的字节数(返回值是⼀个"signed size" 类型,常常是⽬标平台本地的整数类型).ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);// 发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 返回值代表成功写的字节数.ssize_t (*aio_read)(struct kiocb *, const struct iovec *, unsigned long,loff_t);// 初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.ssize_t (*aio_write)(struct kiocb *, const struct iovec *, unsigned long,loff_t);// 初始化设备上的⼀个异步写.int (*readdir)(struct file *, void *, filldir_t);// 对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.unsigned int (*poll)(struct file *, struct poll_table_struct *);int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);long (*compat_ioctl)(struct file *, unsigned int, unsigned long);int (*mmap)(struct file *, struct vm_area_struct *);// mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤返回 - ENODEV.int (*open)(struct inode *, struct file *);// 打开⼀个⽂件int (*flush)(struct file *, fl_owner_t id);// flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;int (*release)(struct inode *, struct file *);// 在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.int (*fsync)(struct file *, struct dentry *, int datasync);// ⽤⼾调⽤来刷新任何挂着的数据.int (*aio_fsync)(struct kiocb *, int datasync);int (*fasync)(int, struct file *, int);int (*lock)(struct file *, int, struct file_lock *);// lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实现它.ssize_t (*sendpage)(struct file *, struct page *, int, size_t, loff_t *,int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock)(struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **);
};
5.文件缓冲区
缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
5.1 缓冲方式
- 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/0系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
- 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准1/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行1/0系统调用操作,默认行缓冲区的大小为1024。
- 无缓冲区:无缓冲区是指标准l/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
特殊缓冲触发
- 缓冲区满时;
- 执行flush语句;
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0){perror("open");return 0;}printf("hello world: %d\n", fd);close(fd);return 0;
}
由于缓冲区的原因,并没有数据打印到文本中
此时就使用fflush刷新
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0){perror("open");return 0;}printf("hello world: %d\n", fd);fflush(stdout) ;close(fd);return 0;
}
另一种方式:利用stderr不带缓冲区
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{close(2);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0){perror("open");return 0;}perror("hello world");close(fd);return 0;
}