在 Linux 操作系统中,IO(输入/输出)操作是程序与外部设备(如磁盘、终端、网络等)进行交互的机制。Linux 操作系统中的 IO 操作基本上都是通过 文件描述符 来完成的。实际上,在 Linux 中,几乎所有的设备(包括磁盘、网络、键盘、显示器等)都被抽象为文件,操作系统提供统一的接口来进行读写。
1. 什么是文件描述符呢?
文件描述符就是操作系统用来标识和管理文件的一个“编号”或者“标签”。当程序打开文件时,操作系统会返回一个文件描述符。程序之后通过这个文件描述符来读写文件。比如,你去电影院买票,电影院给你发一个票号,你拿着票号就可以去看电影。
在 Linux 中,几乎一切都被当做“文件”来处理:包括实际的文件、网络连接、设备、管道等等。而 文件描述符 就是操作系统用来管理这些文件的“门票”编号。
2. linux文件类型
1.普通文件(regular):存在于外部存储器中,用于存储普通数据。
2.目录文件(directory):用于存放目录项,是文件系统管理的重要文件类型。
3.管道文件(pipe):一种用于进程间通信的特殊文件,也称为命名管道 FIFO。
4.套接字文件(socket):一种用于网络间通信的特殊文件。
5.链接文件(link):用于间接访问另外一个目标文件,相当于 Windows 快捷方式。
6.字符设备文件(character):字符设备在应用层的访问接口。
7.块设备文件(block):块设备在应用层的访问接口。
3. IO系统调用(部分)
1)open()
int open(const char *pathname, int flags, mode_t mode);
pathname:文件路径
flags:打开文件的方式
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以可读写方式打开文件
上述三种flag 是互斥的,只能选择其中一个,还有一些其他的flags 可以通过 或( | )来进行组合,例如:
O_CREAT 若欲打开的文件不存在则自动建立该文件
O_TRUNC 若文件存在并且以可写的方式打开时, 此旗标会令文件长度清为 0, 而原来存于该文件的资料也会消失
O_APPEND 当读写文件时会从文件尾开始移动, 也就是所写入的数据会以附加的方式加入到文件后面
等等......
mode :如果改文件是新建的,则可以指定其访问权限
在linux中我们通过 ll 可以查看文件列表,其中会有对改文件权限的描述

其实就是drwxrwxrwx,最前面的d表示该文件是一个目录,如果为-则其类型是一个文件,后面三组rwx实则对应的是对用户规定的权限, 分别对应着所有者(user)、用户组(group)和其他用户(others);r代表着读,w代表写,x代表可执行。这三个权限可以用二进制数字表示,如111对应着rwx,表示r(读)+w(写)+x(可执行);110对应rw-表示r(读)+w(写),如果没有对应的权限用-表示。
在通过mode设置权限的时候,我们就可以用三个八进制数来表示 。当你希望创建一个文件并给它设置 所有者、用户组、其他用户都有读和写权限 时,可以指定 mode 为 0666:
所有者(user):读(r)+ 写(w) = 6
用户组(group):读(r)+ 写(w) = 6
其他用户(others):读(r)+ 写(w) = 6
为什么要在权限前面添加 0 ?
在 C 语言中,数字前面的 0 是八进制数的标志。如果没有加 0,它会被默认当作十进制数来处理,而你传递的值就不是你期望的八进制值了。
示例:
//引入对应的头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
int fd = open("example.txt",O_RDONLY | O_CREAT ,0444);//以只读的方式打开,如果文件不存在则创建一个,权限r--r--r--
if (fd ==-1)
{
printf("打开文件失败!\n");
}
return 0;
}
2)read() 和write()
read() 和 write() 是最基本的 IO 系统调用,它们分别用来从文件或设备中读取数据,或将数据写入文件或设备中。
ssize_t read(int fd, void *buf, size_t count);
ssize_t write (int fd, const void * buf, size_t count);
fd :文件描述符 (如open()返回值)
buf:缓冲区,存放要读取或写入的数据。
count:要读取或写入的字节数
示例:
char buf[20];
read(fd, buf, sizeof(buf)); // 从文件中读取数据
write(fd, buf, sizeof(buf)); // 输出到标准输出(屏幕)
3)close()
用来关闭打开的文件描述符,释放相关资源。
int close(int fd);
4)lseek()
用于移动文件指针到指定的位置。它允许你在打开的文件中定位当前读写位置,支持随机访问文件内容。
off_t lseek(int fd, off_t offset, int whence);
offset:偏移量,指定文件指针移动的字节数。
whence:指定偏移量的基准位置,比如 SEEK_SET(从文件开头)、SEEK_CUR(从当前位置)、SEEK_END(从文件末尾)。
示例:
int main(int argc, char const *argv[])
{
int fd = open("example.txt",O_WRONLY | O_CREAT ,0444);
if (fd ==-1)
{
printf("打开文件失败!\n");
}
lseek(fd,0,SEEK_END);//在尾部添加
char* buf = "nothing is impossible";
ssize_t len = write(fd,buf,strlen(buf));
return 0;
}
原来example.txt中

执行后,写入“nothing is impossible”

lseek()在现实中其实离我们很近。在我们下载的时候,当你下载到一半,下载中断(网络波动或者程序崩溃等原因),在重新下载的时候,通常会找到之前下载的位置后,继续下载。在这个过程中其实lseek()就发挥着重要的作用,lseek() 被用来 定位文件指针,使其指向文件的正确位置,继续下载。
open()、close()、write() 等是系统调用,除了这些以外,还标准库中还有fopen()、fclose()、fwrite()。为什么还需要这些标准库函数呢?
4. 系统调用VS 标准库函数
系统调用:
-
系统调用如
open()、close()、read()和write()是直接与操作系统内核交互的接口。 -
它们提供了最基本的文件操作功能,操作系统通过这些调用管理文件的打开、关闭、读写等。
-
系统调用直接处理文件描述符,并且通常比较低级,能够提供最大程度的控制。
标准库函数:
-
标准库函数如
fopen()、fclose()、fwrite()等则是基于系统调用构建的更高层次的抽象接口。它们提供了更便捷的接口,通常会处理更多的细节。 -
标准库函数通常在程序员和操作系统之间提供了一个中间层,简化了文件操作的过程。
-
它们依赖于缓冲区来优化 I/O 操作,这使得文件操作更加高效,尤其是在进行大量 I/O 操作时。
主要优势
缓冲机制:
fopen() 打开文件时会启用 缓冲机制,默认使用 内存缓冲区,这意味着文件的读写操作不会每次都直接与磁盘交互,而是先写入内存缓存区,只有缓存区满了或者手动刷新时,才会将数据写回磁盘。
这种机制提高了文件操作的性能,因为操作系统不需要频繁地进行磁盘 I/O,而是将多次小的操作合并为一次大的操作。
文件指针:
fopen():返回一个 文件指针(FILE*),它是一个指向 FILE 结构的指针,FILE 结构包含了文件的元数据(如缓冲区、文件位置指针等)。fopen() 内部会处理很多额外的功能,例如缓冲、字符编码转换等。它使得程序员不需要自己处理文件描述符和底层的 I/O 操作。
ps:什么是缓冲区?
缓冲区 就像是一个 汽车,而文件的 数据 就像是乘客。没有缓冲区的情况下,每一个数据单元(就像每一个乘客)都需要单独发送,就像每来一个人就立刻发车,效率低下;而有缓冲区的情况下,汽车会 等到一定数量的乘客(数据)汇集在一起,才会 发车,这样可以减少频繁发车的次数,提高效率。
5. 标准库函数(部分)
1)fopen()
FILE *fopen(const char *filename, const char *mode)
filename:要打开的文件的路径(字符串形式)。
mode:打开文件的模式,指定文件打开的方式和权限。
mode 参数决定了文件的打开方式,常见的模式有:
-
"r":以只读方式打开文件。如果文件不存在,返回NULL。 -
"w":以写入方式打开文件。如果文件不存在,会创建新文件;如果文件已存在,文件内容会被清空。 -
"a":以追加模式打开文件。如果文件不存在,创建新文件;如果文件已存在,数据将写入文件末尾。 -
"r+":以读写方式打开文件。如果文件不存在,返回NULL。 -
"w+":以读写方式打开文件。如果文件不存在,会创建新文件;如果文件已存在,文件内容会被清空。 -
"a+":以读写方式打开文件。如果文件不存在,创建新文件;如果文件已存在,数据将写入文件末尾。
返回值:
-
如果打开文件成功,返回一个指向文件的
FILE *指针。 -
如果打开失败,返回
NULL,并且可以通过errno或perror()来查看错误原因。
2)fprintf()
int fprintf(FILE *stream, const char *format, ...)
fprintf其实和printf类似,只不过前者是将内容输出到文件中,而后者是将内容输出到控制台。
返回值:如果成功,则返回写入的字符总数,否则返回一个负数。
3)fclose()
关闭流 stream。刷新所有的缓冲区。如果流成功关闭,则该方法返回零。如果失败,则返回 EOF。
int fclose(FILE *stream)
在 C 中,文件操作通常是缓冲的,尤其是通过 fopen() 打开的文件。数据会先写入内存中的缓冲区,直到缓冲区被填满或者文件被关闭时,数据才会被真正写入磁盘。fclose() 会自动刷新缓冲区,将内存中的所有待写数据写入到文件中。如果你没有调用 fclose(),缓冲区中的数据可能永远不会被写入磁盘,从而导致数据丢失。
4)fseek()
int fseek(FILE *stream, long int offset, int whence)
stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
offset -- 这是相对 whence 的偏移量,以字节为单位。
whence -- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
| 常量 | 描述 |
|---|---|
| SEEK_SET | 文件的开头 |
| SEEK_CUR | 文件指针的当前位置 |
| SEEK_END | 文件的末尾 |
5)fgetc()
从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。
int fgetc(FILE *stream)
返回值:该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
6)fputc()
把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
int fputc(int char, FILE *stream)
示例:
int main(int argc, char const *argv[])
{
FILE* file = fopen("example.txt","a+");
if (file ==NULL)
{
printf("打开文件失败!\n");
}
int l =fprintf(file, "%s", "persist to the end!");
printf("字符长度为:%d\n",l);
fseek(file,-1,SEEK_END);//负数表示往前偏移,SEEK_END表示文件的末尾,得到的字符应该是“!”
printf("该字符为:%c\n",fgetc(file));
fseek(file,-9,SEEK_CUR);
fputc('A',file);
fclose(file);
return 0;
}
运行结果:

注:
在代码中,使用fseek往前偏移了九个字符,再调用fputc时为什么还是添加在结尾呢?


那是因为我们在使用fopen 的时候选择的是a+。如果文件是以追加模式("a" 或 "a+")打开的,那么文件指针会始终被移动到文件末尾。当执行 fseek(file, -9, SEEK_CUR); 时,尽管你是相对于当前位置回退了 9 个字节,文件指针的位置并没有改变,因为文件是以追加模式打开的。在追加模式下,fseek 不能改变文件的实际写入位置,因为操作系统会始终将写入位置锁定在文件末尾。所以即使你回退了 9 个字节,fputc('A', file); 仍然会将 'A' 写入文件的末尾。
6. 总结
在 Linux 操作系统中,IO 操作是程序与外部设备交互的关键,通过文件描述符实现对文件、设备、网络等资源的管理。Linux 将几乎所有的设备都抽象为文件,使得对它们的操作都可以通过统一的接口进行。通过系统调用如 open()、read()、write() 和 close(),以及标准库函数如 fopen()、fprintf()、fseek() 等,程序可以方便地进行文件读写和数据存取。文件权限、缓冲机制以及文件指针的管理等都对系统性能和数据完整性至关重要。理解这些基础的 IO 操作有助于提高程序的效率和稳定性,尤其在处理大规模数据、网络通信和文件管理时至关重要。
657

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



