Linux:认识基础IO

1.理解"⽂件"

1.1狭义理解

  • ⽂件在磁盘⾥
  • 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的
  • 磁盘是外设(即是输出设备也是输⼊设备)
  • 磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出 简称 IO

1.2广义理解

Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘...... 这些都是抽象化的过程)

1.3⽂件操作的归类认知

  • 对于 0KB 的空⽂件是占⽤磁盘空间的
  • ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容)
  • 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作

1.4系统角度

  • 对⽂件的操作本质是进程对⽂件的操作

  • 磁盘的管理者是操作系统

  • ⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的

2.C文件接口

fopen():打开文件

cpp 复制代码
#include <stdio.h>

//FILE *fopen(const char *path, const char *mode);
int main()
{
	FILE* fp = fopen("myfile", "w");
	if (!fp) {
		printf("fopen error!\n");
	}
	while (1);
	fclose(fp);
	return 0;
}
  • path:文件路径。
  • mode:打开模式(见下表)。
  • 成功:返回 FILE* 流指针。
  • 失败:返回 NULL,并设置 errno

文件打开模式​

模式 说明 文件不存在时 文件存在时
"r" 只读 失败 打开
"w" 只写(清空文件) 创建 清空
"a" 追加(写入文件末尾) 创建 保留内容,追加
"r+" 读写(从文件头开始) 失败 打开
"w+" 读写(清空文件) 创建 清空
"a+" 读写(追加到文件末尾) 创建 保留内容,可读写

fwrite():写入文件​

cpp 复制代码
#include <stdio.h>
#include <string.h>

//size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int main()
{
	FILE* fp = fopen("myfile", "w");
	if (!fp) {
		printf("fopen error!\n");
	}
	const char* msg = "hello bit!\n";
	int count = 5;
	while (count--) {
		fwrite(msg, strlen(msg), 1, fp);
        //fwrite(msg, 1, strlen(msg), fp);
	}
	fclose(fp);
	return 0;
}
  • ptr:要写入的数据指针。
  • size:每个数据项的字节数(如 sizeof(int))。
  • nmemb:要写入的数据项数量。
  • streamFILE* 流。
  • 成功:返回实际写入的数据项数量(可能小于 nmemb)。
  • 失败:返回 0EOF(需检查 ferror(fp)

差异

写法 参数解释 返回值意义
fwrite(msg, strlen(msg), 1, fp) msg 视为 ​​1 个数据块​ ​,每个块大小为 strlen(msg) 字节 成功时返回 1(写入 1 个块)
fwrite(msg, 1, strlen(msg), fp) msg 视为 ​strlen(msg) 个数据项​​,每个项大小为 1 字节(即逐字节) 成功时返回 strlen(msg)

区别

特性 fwrite(msg, strlen(msg), 1, fp) fwrite(msg, 1, strlen(msg), fp)
​写入粒度​ 整个字符串作为 1 个块 逐字节写入
​返回值​ 1(成功)或 0(失败) 实际写入的字节数(可能部分成功)
​错误处理​ 全或无(要么全部成功,要么完全失败) 可检测部分写入
​适用场景​ 必须完整写入的敏感数据(如配置文件) 流式数据或允许部分写入(如日志、网络)

fread():读取文件​

cpp 复制代码
#include <stdio.h>
#include <string.h>

//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
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) {
		//注意返回值和参数
		ssize_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;
}
  • ptr:存储读取数据的缓冲区。
  • size:每个数据项的字节数。
  • nmemb:要读取的数据项数量。
  • streamFILE* 流。
  • 成功:返回实际读取的数据项数量(可能小于 nmemb)。
  • ​EOF​ :返回 0(需用 feof(fp) 检查是否到达文件末尾)。
  • 失败:返回 0EOF(检查 ferror(fp))。

stdin & stdout & stderr

  • C默认会打开三个输⼊输出流,分别是stdin, stdout, stderr
  • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,⽂件指针
cpp 复制代码
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

3.系统⽂件I/O

3.1⼀种传递标志位的⽅法

cpp 复制代码
#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.2系统接口

open():打开文件​

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


//int open(const char *pathname, int flags, mode_t mode);

int fd = open("data.txt", O_RDWR | O_CREAT, 0644); // 读写模式,不存在则创建
if (fd == -1) {
    perror("open failed");
    exit(1);
}
  • pathname:文件路径(如 "./test.txt")。
  • flags:打开方式(见下表)。
  • mode(可选):文件权限(仅在 O_CREAT 时有效,如 0644)。
  • 成功:返回文件描述符。
  • 失败:返回 -1,并设置 errno
标志 说明
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
O_CREAT 文件不存在时创建(需指定 mode
O_TRUNC 若文件存在,清空内容
O_APPEND 追加写入(避免并发写入冲突)

read():读取文件​

cpp 复制代码
#include <unistd.h>
//ssize_t read(int fd, void *buf, size_t count);

char buf[1024];
ssize_t bytes_read = read(fd, buf, sizeof(buf));
if (bytes_read == -1) {
    perror("read failed");
} else if (bytes_read == 0) {
    printf("EOF reached\n");
} else {
    printf("Read %zd bytes: %.*s\n", bytes_read, (int)bytes_read, buf);
}
  • fd:文件描述符(由 open() 返回)。
  • buf:存储读取数据的缓冲区。
  • count:要读取的最大字节数。
  • 成功:返回实际读取的字节数(可能小于 count)。
  • 文件结束(EOF):返回 0
  • 失败:返回 -1,并设置 errno

write():写入文件​

cpp 复制代码
#include <unistd.h>

//ssize_t write(int fd, const void *buf, size_t count);

const char *msg = "Hello, world!\n";
ssize_t bytes_written = write(fd, msg, strlen(msg));
if (bytes_written == -1) {
    perror("write failed");
} else if (bytes_written < strlen(msg)) {
    printf("Partial write: %zd/%zu bytes\n", bytes_written, strlen(msg));
}
  • fd:文件描述符。
  • buf:要写入的数据指针。
  • count:要写入的字节数。
  • 成功:返回实际写入的字节数(可能小于 count)。
  • 失败:返回 -1,并设置 errno

关键注意事项​

​(1) 文件描述符 vs FILE*

  • open() 返回 int 文件描述符,需用 read()/write() 操作。
  • fopen() 返回 FILE*,需用 fread()/fwrite() 操作。
  • ​不要混用​ :例如用 write() 写入 fopen() 打开的文件。

(2) 缓冲区别​

  • write():无缓冲,数据直接进入内核缓冲区(但不一定立即落盘)。
  • fwrite():带缓冲,数据先存到 stdio 缓冲区,满后才调用 write()

3.3先来认识⼀下两个概念: 系统调⽤ 和 库函数

  • 上⾯的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
  • ⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝。


系统调⽤接⼝和库函数的关系,⼀⽬了然。
所以,可以认为, f# 系列的函数,都是对系统调⽤的封装,⽅便⼆次开发。

3.4⽂件描述符fd

通过对open函数的学习,我们知道了⽂件描述符就是⼀个⼩整数

3.4.1 0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的⽂件描述符,分别是标准输⼊0, 标准输出1, 标准错误2.
  • 0,1,2对应的物理设备⼀般是:键盘,显⽰器,显⽰器

所以输⼊输出还可以采⽤如下⽅式:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
	char buf[1024];
	ssize_t s = read(0, buf, sizeof(buf));
	if (s > 0) {
		buf[s] = 0;
		write(1, buf, strlen(buf));
		write(2, buf, strlen(buf));
	}
	return 0;
}

现在知道,⽂件描述符就是从0开始的⼩整数。当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,⽂件描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

3.4.2文件描述符的分配规则

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

输出发现是 fd: 3

关闭0或者2,在看

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	close(0);
	//close(2);
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

发现是结果是: fd: 0 或者 fd 2 ,可⻅,⽂件描述符的分配规则:在files_struct数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。

3.4.3重定向

那如果关闭1呢?看代码:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
	close(1);
	int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	fflush(stdout);
	close(fd);
	exit(0);
}

此时,我们发现,本来应该输出到显⽰器上的内容,输出到了⽂件 myfile 当中,其中,fd=1。这
种现象叫做输出重定向。常⻅的重定向有: > , >> , <

3.4.4使⽤ dup2 系统调⽤

函数原型

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);

示例

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
	int fd = open("./log", O_CREAT | O_RDWR);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	close(1);
	dup2(fd, 1);
	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;
}

printf是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问⽂件的时候,找的还是fd:1, 但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。

4.缓冲区

4.1什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区。

4.2为什么要引⼊缓冲区机制

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区可⼤ 提⾼计算机的运⾏速度。

4.3缓冲类型

标准I/O提供了3种类型的缓冲区

  • 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。
  • ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
  • ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。
    除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
  • 缓冲区满时;
  • 执⾏flush语句;

4.4FILE

因为IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调⽤,所以本质上,访问⽂件都是通
过fd访问的。
所以C库当中的FILE结构体内部,必定封装了fd。

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
	const char* msg0 = "hello printf\n";
	const char* msg1 = "hello fwrite\n";
	const char* msg2 = "hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}

运⾏出结果:

cpp 复制代码
hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:

cpp 复制代码
hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现 printf 和 fwrite (库函数)都输出了2次,⽽ write 只输出了⼀次(系统调⽤)。为
什么呢?肯定和fork有关

  • ⼀般C库函数写⼊⽂件时是全缓冲的,⽽写⼊显⽰器是⾏缓冲。
  • printf fwrite 库函数+会⾃带缓冲区,当发⽣重定向到普通⽂件时,数据的缓冲⽅式由⾏缓冲变成了全缓冲。
  • ⽽我们放在缓冲区中的数据,就不会被⽴即刷新,甚⾄fork之后
  • 但是进程退出之后,会统⼀刷新,写⼊⽂件当中。
  • 但是fork的时候,⽗⼦数据会发⽣写时拷⻉,所以当你⽗进程准备刷新的时候,⼦进程也就有了同样的⼀份数据,随即产⽣两份数据。
  • write 没有变化,说明没有所谓的缓冲。
    综上: printf fwrite 库函数会⾃带缓冲区,⽽ write 系统调⽤没有带缓冲区。另外,我们这
    ⾥所说的缓冲区,都是⽤⼾级缓冲区。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调⽤,库函数在系统调⽤的
"上层", 是对系统调⽤的"封装",但是 write 没有缓冲区,⽽ printf fwrite 有,⾜以说
明,该缓冲区是⼆次加上的,⼜因为是C,所以由C标准库提供。

相关推荐
gbase_lmax12 分钟前
gbase8s数据库 tcp连接不同阶段的超时处理
网络·数据库·网络协议·tcp/ip
熬夜学编程的小王22 分钟前
【Linux篇】多线程编程中的互斥与同步:深入理解锁与条件变量的应用
linux·条件变量·线程同步·线程互斥
Aliano21723 分钟前
Pinecone向量库 VS Redis
数据库·redis·缓存·pinecone向量库
爬呀爬的水滴28 分钟前
02 mysql 管理(Windows版)
数据库·mysql
IT成长日记39 分钟前
【Hive入门】Hive增量数据导入:基于Sqoop的关系型数据库同步方案深度解析
数据库·hive·sqoop·关系型数据库同步·增量数据导入
christine-rr1 小时前
【25软考网工】第五章(8)路由协议RIP、OSPF
运维·网络·网络工程师·软考·考试
芯辰则吉--模拟芯片1 小时前
模拟Sch LVS Sch 方法
服务器·数据库·lvs
weixin_437044641 小时前
JumpServer批量添加资产
数据库·mysql
漫谈网络1 小时前
SSHv2 密钥交换(Key Exchange)详解
运维·ssh·自动化运维·devops·paramiko·sshv2
cyhysr1 小时前
oracle 触发器与commit的先后执行顺序
数据库·oracle