【从浅学到熟知Linux】基础IO第一弹=>C语言文件操作接口、文件系统调用、文件描述符概念及分配规则

🏠关于专栏:Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程等内容。

🎯每天努力一点点,技术变化看得见

文章目录


C语言文件接口回顾

在开始介绍基础IO上篇的相关内容前,让我们先巩固一下C语言的文件操作

C语言中打开文件的方式及区别如下标所示↓↓↓

打开模式 描述
r 只读方式打开
r+ 以读写方式,读写开始位置默认在文件开始
w 以写方式打开,文件不存在则创建,存在则清空
w+ 以读写方式打开,文件不存在则创建,存在则清空
a 以追加方式打开,文件不存在则创建,文件存在则在文件末尾追加写入
a+ 以追加与读方式打开,文件不存在则创建,读写位置默认在文件尾

下面是三个读写文件的函数

c 复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fprintf(FILE *stream, const char *format, ...);

【代码示例1】r(只读)方式打开,结合fread函数↓↓↓

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

int main()
{
	FILE* fp = open("./log.txt", "r");
	char buffer[1024];
	fread(buffer, sizeof(buffer), sizeof(char), fp);
	printf("%s\n", buffer);
	fclose(fp);
	return 0;
}

【代码示例2】r+(读写)方式打开,结合fwrite、fread函数↓↓↓

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

int main()
{
	FILE* fp = fopen("./log.txt", "r+");
	char* msg = "xm";
	fwrite(msg, strlen(msg), sizeof(char), fp);
	char buffer[1024];
	fread(buffer, sizeof(buffer) - 1, sizeof(char), fp);
	printf("%s\n", buffer);
	fclose(fp);
	return 0;
}

【代码示例3】w(写)方式打开,结合fprintf函数↓↓↓

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

int main()
{
	FILE* fp = fopen("./log.txt", "w");
	fprintf(fp, "jammingpro\n");
	fclose(fp);
	return 0;
}

【代码示例3】w+(读写)方式打开,结合fseek、fread、fwrite函数↓↓↓

★ps:fseek的接口声明如下↓↓↓

c 复制代码
int fseek(FILE *stream, long offset, int whence);

其中whence和offset可填写值和关系如下:

whence offset
SEEK_SET 从文件开始位置向后偏移n个位置
SEEK_CUR 从当前位置向后偏移n个位置
SEEK_END 从文件结尾位置向前偏移n个位置
c 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
	FILE* fp = fopen("./log.txt", "w+");
	char* msg = "jammingpro\n";
	fwrite(msg, strlen(msg), sizeof(char), fp);
	fseek(fp, 0, SEEK_SET);
	char buffer[1024];
	fread(buffer, sizeof(buffer), sizeof(char), fp);
	printf("%s", buffer);
	fclose(fp);
	return 0;
}

【代码示例4】a(追加)方式打开,结合fwrite函数↓↓↓

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

int main()
{
	FILE* fp = fopen("./log.txt", "a");
	char* msg = "Jammingpro\n";
	fwrite(msg, strlen(msg), sizeof(char), fp);
	fclose(fp);
	return 0;
}

【代码示例5】a+(读写)方式打开,结合fseek、fread、fwrite函数↓↓↓

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

int main()
{
	FILE* fp = fopen("./log.txt", "a+");
	char* msg = "Jammingpro\n";
	fwrite(msg, strlen(msg), sizeof(char), fp);
	fseek(fp, 0, SEEK_SET);
	char buffer[1024];
	fread(buffer, sizeof(buffer), sizeof(char), fp);
	fclose(fp);
	return 0;
}

系统文件概念与接口

文件基本概念

当我们使用ls -l查看文件信息时,会显示文件的类型、权限、引用计数、所有者、所属组、大小、创建时间等信息,这些都是文件一些属性信息。

由此,我们可以知道:文件=内容+属性

文件被打开,需要先被加载到内存。由于内存空间有限,不可能将所有文件全部打开,所以文件可分为已经被打开的文件和没有被打开的文件。

对于打开的文件,由于文件必然是被进程打开的,因而我们需要研究文件与进程之间的关系;对于没有打开的文件,这些文件一定存放于磁盘中,这么多的未打开文件如何被分门别类的存放于磁盘中,要如何快速找到它们并对它们做增删改查操作是我们需要研究的问题。对于打开的文件,将在该文章中介绍,未打开的文件将在下一篇文章介绍。

系统接口

文件其实是在磁盘上的,磁盘属于外部设备,访问磁盘上的文件就是访问磁盘。进程要对文件做操作就需要访问硬件设备,而整个计算机中只有操作系统具有这种权力,操作系统会提供对应的调用接口用于访问对应的硬件设备。下面介绍一下操作系统提供的文件操作接口↓↓↓

open

open系统接口第一个参数需要传入待打开文件的路径,第二个参数表示以什么样的方式打开,第二个参数可填写的选项如下标所示↓↓↓

flags选项 描述
O_RDONLY 只读
O_WRONLY 只写
O_CREAT 不存在就创建
O_TRUNC 清空文件
O_APPEND 追加写入

这些选项如何使用?使用原理是什么呢?它的原理就是位图结构。一个int类型包含32个比特位,如果我们让低位起第1位为1表示READ,低位起第2位为1表示WRITE...(如下图所示)

此时若要实现CREAT、TRUNC、WRITE,则只要对这三个数做或运算,即CREAT | TRUNC | WRITE,则会得到一个值为00000000 00000000 00000000 00011010的flags。将它传递给处理函数中,处理函数会将flags与READ、WRITE、APPEND等,挨个做与运算,如果与出来的结果不为0,则表示该选项被选择。

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

#define READ	(1 << 0)
#define WRITE	(1 << 1)
#define APPEND	(1 << 2)
#define CREAT	(1 << 3)
#define TRUNC	(1 << 4)

void run(int flags)
{
	if(flags & READ)
	{
		printf("reading...\n");
	}
	if(flags & WRITE)
	{
		printf("writing...\n");
	}
	if(flags & APPEND)
	{
		printf("appending...\n");
	}
	if(flags & CREAT)
	{
		printf("creating...\n")
	}
	if(flags & TRUNC)
	{
		printf("truncing...\n");
	}
}

int main()
{
	run(READ);
	printf("------------------------------------\n");
	run(WRITE | TRUNC | CREAT);
	printf("------------------------------------\n");
	run(APPEND | CREAT);
	printf("------------------------------------\n");
	return 0;
}

而open的第三个参数表示表示文件创建时的权限。当我们使用touch命令创建文件时,我们习以为常的认为,文件的默认权限是666,文件夹的默认权限是777。但在使用系统接口时,我们需要显示指明文件权限,系统接口没有给我们设置默认权限。

★ps:系统文件权限默认是666,目录默认权限是777。但它们的默认权限需要与~umask做与运算,即文件的最终权限等于666&~mask,目录的最终权限是777&~umask。我们可以使用umask接口手动设置umask,而不使用当前系统的默认umask。(umask的修改只在该程序内有效)

下面,我们使用系统接口open以O_RDONLY | O_CREAT创建一个文件↓↓↓(由于没有写明创建文件时的权限,故创建的文件权限是随机的)

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

int main()
{
	int fd = open("./log.txt", O_RDNOLY | O_CREAT);
	printf("%d\n", fd);
	return 0;
}

下面,我们使用系统接口open以O_TRUNC | O_CREAT创建一个权限为0666的文件↓↓↓(在创建文件前,我们需要umask权限掩码清零)

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

int main()
{
	umask(0);
	int fd = open("./log.txt", O_TRUNC | O_CREAT, 0666);
	printf("%d\n", fd);
	return 0;
}

★ps:open打开文件后会返回一个数字,该数字称为文件描述符,用于唯一标识当前进程打开的文件(即同时打开的不同文件的文件标识符不同),下文将对文件描述符做详细介绍

read

要从某个文件中读取内容时,第一个参数需要传入该文件的文件描述符,第二个参数需要传入接收文件内容的缓冲区首地址,第三个参数表示要从文件中读取多少字节的内容。

下面程序模拟实现了C语言fopen的r(只读)打开模式↓↓↓

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("./log.txt", O_RDONLY);
	char buffer[1024];
	read(fd, buffer, sizeof(buffer));
	printf("%s", buffer);
	return 0;
}

write

要向某个文件中写入内容时,第一个参数需要传入该文件的文件描述符,第二个参数需要传入将要写入文件的字符串的地址,第三个参数表示要向文件中写入多少字节的内容。

下面程序模拟实现C语言fopen的w(只读)模式打开↓↓↓

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	char* msg = "Have a good day!\n";
	write(fd, msg, strlen(msg));
	return 0;
}

下面程序模拟实现C语言fopen的a(追加)模式打开↓↓↓

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("./log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
	char* msg = "Have a good day!\n";
	write(fd, msg, strlen(msg));
	return 0;
}

close

close用于关闭文件描述符对应的文件,它只需要传入文件描述符即可。该处不做代码演示,在lseek的示例代码中会演示close的使用。

lseek

lseek与C语言的fseek使用方法类似,用于移动文件的读写位置,不同之处在于fseek第一个参数传入的是FILE*类型的FILE结构体指针,而lseek第一个参数传入的是文件描述符。第三个描述符仍可以填写以下三个中的一个:SEEK_SET(文件头)、SEEK_CUR(当前位置)、SEEK_END(文件尾),而第二个参数表示偏移量。

下面给出代码示例↓↓↓

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("./log.txt", O_RDONLY | O_APPEND, 0666);
	char buffer[1024];
	read(fd, buffer, sizeof(buffer));
	printf("%s", buffer);
	//回到文件头再读一遍
	lseek(fd, 0, SEEK_SET);
	read(fd, buffer, sizeof(buffer));
	printf("%s", buffer);
	close(fd);
	return 0;
}

什么是当前路径

当我们没有指名open打开文件的路径时,则open将在当前路径下创建文件↓↓↓

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("log.txt", O_CREAT);
	close(fd);
	sleep(60);
	return 0;
}

将程序运行起来后,我们可以使用ps axj | grep test查看运行该程序的进程pid,进入/proc/进程pid目录,可以看到两个链接文件cwd和exe。其中,cwd是程序的工作路径,也就是我们常说的当前路径,而exe是可自行程序的保存位置。

当程序运行起来后,默认的工作路径是执行该程序时bash所处的路径。如果我们位于/home/xiaoming路径下执行上述程序,则cwd则指向/home/xiaoming。

★ps:几乎所有的库只要访问硬件设备,必须要封装系统调用。故C语言的文件操作接口是对上述系统调用接口的封装。

文件描述符

文件描述符概念与原理

通过上述内容,我们知道了文件描述符就是一个整数。下面我们来聊一聊文件描述符的内容。

C语言程序运行时,默认会打开3个输入输出流,我们可以使用一段代码来验证一下↓↓↓

★ps:C语言中接收fopen返回值的类型是FILE,C语言库函数是对系统接口的封装,故FILE中必然存在一个保存描述符的变量,即_fileno。

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

int main()
{
	printf("stdin->%d\n", stdin->_fileno);
	printf("stdout->%d\n", stdout->_fileno);
	printf("stderr->%d\n", stderr->_fileno);
	return 0;
}

C语言默认打开3个输入输出流是语言特性吗?其实不是,这是操作系统的特性。操作系统中,进程创建时默认会打开0、1、2号描述符,它们的详细内容如下标↓↓↓

文件描述符 描述 对应设备
0 标准输入 键盘
1 标准输出 显示器
2 错误输出 显示器

下面,我们使用系统接口从0号描述符读入内容,并将读入内容写入1号及2号描述符↓↓↓

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>

int main()
{
	char buffer[1024];
	read(0, buffer, sizeof(buffer));
	printf("write to 1st fd:\n");
	write(1, buffer, strlen(buffer));
	printf("write to 2nd fd:\n");
	write(2, buffer, strlen(buffer));
	return 0;
}

★ps:向1号和2号描述符中打印都是输出到显示器,那它们有什么区别呢?

当我们执行上述程序时使用./sysfileno > ouput.txt,再查看文件输出结果:打印到1号描述符的(printf默认打印到1号描述符)内容被写入到output.txt,而打印到2号描述符的内容的内容被输入到屏幕了。

./sysfileno > ouput.txt./sysfileno 1> ouput.txt是等价的,两者没有区别↓↓↓

那如果要将打印到2号描述符的内容打印到err.txt中,打印到1号描述符的内容打印到屏幕呢?则需要执行:./sysfileno 2> ouput.txt↓↓↓

如果需要将写入到1号和2号文件描述符的内容输入到result.txt,则需要执行./sysfileno > result.txt 2>&1./sysfileno 2> result.txt 1>&2↓↓↓(两者输出结果相同)

当操作系统中打开大量的文件时,就需要对打开的文件做管理。在Linux系统中,使用struct file结构体保存打开文件的信息;而对于每个进程来说,每个进程都维护着一个struct files_struct,struct files_struct保存着当前进程打开的各个文件的struct file地址。(每个打开的文件,在内核当中都有file对象,保存了文件相关的inode元信息)ps:inode将于专栏下一篇文章介绍

现在知道:文件描述符就是从0开始的小整数(因为数组的下标是从0下标开始的)。当打开文件时,操作系统在内存中要创建相应的结构体来描述目标文件,于是就有了file结构体(用于表示一个已经打开的文件对象)。当进程打开文件时,为了让进程和该进程打开的文件产生关联,每个进程就有了一个*files,指向一张files_struct表,该表最重要的部分是:包含了一个保存已打开文件的file结构体地址的指针数组。

所以,本质上,文件描述符就是数组下标(即当前进程的文件描述符表下标)。只要取得对应的文件描述符,就能找到对应的文件。

文件描述符分配规则

那我们打开一个新的文件时,是如何分配文件描述符的呢?↓↓↓

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>

int main()
{
	printf("stdin->%d\n", stdin->_fileno);
	printf("stdout->%d\n", stdout->_fileno);
	printf("stderr->%d\n", stderr->_fileno);
	
	int fd1 = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	printf("fd1->%d\n", fd1);
	
	//关闭标准输入
	close(0);
	int fd2 = open("./log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	printf("fd2->%d\n", fd2);

	//关闭标准错误
	close(2);
	int fd3 = open("./log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	printf("fd3->%d\n", fd3);

	close(fd1);
	close(fd2);
	close(fd3);
	return 0;
}

系统默认打开三个标准输入输出流stdin(0号)、stdout(1号)、stderr(2号),此时我们打开一个文件时分配3号描述符,因为0到2号均被占用;关闭0号,再打开一个新文件时,该新文件的文件描述符为0;关闭2号,再打开一个新闻界时,该文件的文件描述符为2。由此可知,文件描述的分配规则是:分配当前最小的未使用的文件描述符

🎈欢迎进入从浅学到熟知Linux专栏,查看更多文章。

如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d

相关推荐
杰哥在此9 分钟前
Python知识点:如何使用Multiprocessing进行并行任务管理
linux·开发语言·python·面试·编程
ROBIN__dyc16 分钟前
C语言基本概念
c语言·开发语言
枫叶丹42 小时前
【在Linux世界中追寻伟大的One Piece】进程信号
linux·运维·服务器
刻词梨木2 小时前
ubuntu中挂载点内存不足,分配不合理后使用软链接的注意事项
linux·运维·ubuntu
灯火不休ᝰ3 小时前
[win7] win7系统的下载及在虚拟机中详细安装过程(附有下载文件)
linux·运维·服务器
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
powerfulzyh7 小时前
Ubuntu24.04远程开机
linux·ubuntu·远程工作
ulimpid7 小时前
Command | Ubuntu 个别实用命令记录(新建用户、查看网速等)
linux·ubuntu·command
HHoao7 小时前
Ubuntu启动后第一次需要很久才能启动GTK应用问题
linux·运维·ubuntu
小灰兔的小白兔7 小时前
【Ubuntu】Ubuntu常用命令
linux·运维·ubuntu