万字详解linux系列基础io(代码片段)

山舟 山舟     2022-12-01     456

关键词:

文章目录


前言

有关C语言中对文件的操作可以在C语言文件操作中查看。

(1)当前目录

先来看一段代码:

#include <stdio.h>

int main()

	//如果文件不存在,默认在当前目录下创建文件
	FILE* fp = fopen("log.txt", "w");
	if (fp == NULL)
	
		perror("fopen");
		return 1;
	
	fprintf(fp, "hello world!");
	
	fclose(fp);
	return 0;

运行结果如下:

从上可以看出创建的文件与可执行程序在同一目录下,这一理解其实不对,请看下面的例子。


所以当前目录是进程运行时所处的目录,具体可通过下面的方式来查看。

#include <stdio.h>
#include <unistd.h>

int main()

	//如果文件不存在,默认在当前目录下创建文件
	FILE* fp = fopen("log.txt", "w");
	if (fp == NULL)
	
		perror("fopen");
		return 1;
	
	fprintf(fp, "hello world!");
	
	fclose(fp);
	
	while(1)//进程死循环,便于查看进程信息
	
		sleep(1);
		
	return 0;


(2)stdin、stdout、stderr

C语言任何进程默认会打开三个输入输出流,分别是stdin、stdout、stderr(分别对应键盘、显示器、显示器),事实上这三个流的类型都是FILE*,它们本质上都是文件指针。

因为它们都是默认打开的,所以C语言中scanf可以直接从键盘读、printf可以直接向显示器输出。

//下面的写法两两等价
char buffer[1024];
fgets(buffer, 1000, stdin);//从stdin(键盘)读其实等价于scanf
scanf("%s", buffer);

fprintf(stdout, "hello world!");//向stdout(显示器)输出其实等价于printf
fprintf(stderr, "hello world!");//stderr也是显示器,所以这样也可以向显示器输出
printf("hello world!");

一、open

用open来引出系统级别的IO。

(上图只是man中对open最直接的介绍,各种参数及用法并没有放在图中)

pathname是要打开或创建的目标文件。

参数flags有很多,比如O_RDONLY(只读打开)、O_WRONLY(只写打开)、O_RDWR(读,写打开)这三个常量,必须指定一个且只能指定一个;还有O_CREAT(若文件不存在,则创建,且需要使用mode选项,来指明新文件的访问权限)、O_APPEND(追加写)。

返回值:成功则返回新打开的文件的文件描述符(后面会提到),失败则返回-1。


(1) 标志位

flags是一个int类型的参数,而int有32个比特位,把每一位为1都定义为一个宏,在这种规则下就可以定义出32种状态,当需要同时满足多种状态时只需要“或”操作即可。

比如将0x1定义为O_WRONLY、0x20定义为O_CREAT,则它们的二进制序列如下:

00000000 00000000 00000000 00000001 O_WRONLY
00000000 00000000 00000010 00000000 O_CREAT

传入参数后,只需检测flags哪一个比特位为1就可以识别出传入了哪种状态;如果需要同时传入多种状态,只需取“或”运算。

这样只用一个int型的参数就能定义出很多的状态(包括各自的组合)。


(2) O_WRONLY

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()

	int fd = open("log.txt", O_WRONLY);
	printf("fd : %d\\n", fd);

	return 0;


可以看到返回值为-1,说明有错误,且当前目录下并没有log.txt,原因是系统级别的open不同于C语言中的fopen,它在只写且文件不存在时不会自动创建。


(3) O_CREAT

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()

	//			加入O_CREAT,在文件不存在时自动创建
	int fd = open("log.txt", O_WRONLY | O_CREAT);
	printf("fd : %d\\n", fd);

	return 0;


加入O_CREAT后,fd返回值不是-1说明open正常返回,当前目录下也创建出了log.txt,但很明显看到新创建的文件的权限是乱的,log.txt本身也自动用红底标注出来。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()

	umask(0);//将掩码设置为0
	int fd = open("log.txt", O_WRONLY, 0666);//将log.txt的权限设置为0666,注意第一个0不能省略
	printf("fd : %d\\n", fd);

	return 0;

这样一个具有特定权限的log.txt就创建出来了。(有关掩码、权限等可在【万字详解Linux系列】权限管理中查看)


二、close,read,write


像C语言中fclose与fopen对应一样,系统层面的close也和open相对应。

//count是希望读入或写入的个数
//返回实际读入或写入的个数
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

下面代码用系统接口write向文件中写入内容。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()

	umask(0);
	int fd1 = open("log.txt", O_WRONLY, 0666);
	if (fd1 < 0)
	
		printf("open error!\\n");
		return 1;
	

	int count = 5;
	const char* msg = "hello world!\\n";
	while (count--)
	
		//注意最后的参数如果用strlen(msg)+1把'\\0'算上是不对的
		//因为字符串以'\\0'结尾是C语言的规定
		//向文件里写入时不需要管'\\0'
		write(fd1, msg, strlen(msg));
	
	
	close(fd1);
	return 0;

成功创建log.txt并向其中写入了5个hello world!


下面再使用read读取文件内的内容。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()

	umask(0);
	int fd1 = open("log.txt", O_RDONLY, 0666);//以只读的方式打开
	if (fd1 < 0)
	
		printf("open error!\\n");
		return 1;
	

	char c;
	while (1)
	
		//每次向c内读一个字符,num返回读到的字符个数
		ssize_t num = read(fd1, &c, 1);
		if (num <= 0)//如果没有读到字符就退出
			break;

		write(stdout, &c, 1);//向屏幕输出
	
	
	close(fd1);
	return 0;

fopen、fclose、fread、fwrite等都是C标准库(libc)当中的函数,称之为库函数,通过libc这一层封装,在保证可读性的同时也兼顾了跨平台性。而open、close、read、write等等都属于系统提供的接口,是系统调用接口。


三、文件描述符

1.概念

上面open返回的值要么是-1(失败),要么是3,它会是其他值吗?

下面连续创建5个文件,查看每个open的返回值有什么规律。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()

	umask(0);
	int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);
	printf("fd1 : %d\\n", fd1);
	int fd2 = open("log.txt", O_WRONLY | O_CREAT, 0666);
	printf("fd2 : %d\\n", fd2);
	int fd3 = open("log.txt", O_WRONLY | O_CREAT, 0666);
	printf("fd3 : %d\\n", fd3);
	int fd4 = open("log.txt", O_WRONLY | O_CREAT, 0666);
	printf("fd4 : %d\\n", fd4);
	int fd5 = open("log.txt", O_WRONLY | O_CREAT, 0666);
	printf("fd5 : %d\\n", fd5);

	close(fd1);
	close(fd2);
	close(fd3);
	close(fd4);
	close(fd5);
	return 0;

很明显看到5个返回值是从3开始递增的。

-1表示失败,所以中间少了0、1、2三个文件描述符。还记得前言中提到的stdin、stdout、stderr吗?没错,这三个文件对应的文件描述符依次是0、1、2。因为它们是默认已经打开的,所以再创建时文件描述符从3开始依次递增(事实上,文件描述符的本质是数组下标)。

由于1、2代表的特殊意义,前面的代码可以如下修改。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()

	umask(0);
	int fd1 = open("log.txt", O_RDONLY, 0666);//以只读的方式打开
	if (fd1 < 0)
	
		printf("open error!\\n");
		return 1;
	

	char c;
	while (1)
	
		//每次向c内读一个字符,num返回读到的字符个数
		ssize_t num = read(fd1, &c, 1);
		if (num <= 0)//如果没有读到字符就退出
			break;

		//下面三种写法等价
		write(stdout, &c, 1);
		write(1, &c, 1);//1是显示器的文件描述符,即向屏幕输出
		write(2, &c, 1);//2也显示器的文件描述符,即向屏幕输出
	
	
	close(fd1);
	return 0;

如果关闭0、1、2中的一个或几个会发生什么呢?请看下面的代码。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()

	close(1);//关闭1文件描述符,也即关闭显示器
	printf("hello world!\\n");
	return 0;

运行后并没有打印hello world!,因为printf底层就是向显示器(文件描述符为1)中打印内容,但它被关闭了,所以自然无法打印出内容来。同理,如果把文件描述符0关掉,就无法从键盘输入。

2.原理

因为每个进程都可以打开多个文件,而系统中时刻都存在大量运行中的进程,所以也就存在大量的已经打开的文件,而每个文件有包括它的内容和属性,所以文件管理就是操作系统必须做的。Linux中用struct file这个结构体就是来管理文件。


文件描述符就是从0开始的整数。当打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,于是就有了file结构体表示一个已经打开的文件对象。而进程执行IO系统调用必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表包含一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。


3.分配规则

再看下一段代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()

	close(0);
	int fd1 = open("log.txt", O_WRONLY | O_CREAT, 06666);
	printf("fd1 : %d\\n", fd1);
	int fd2 = open("log.txt", O_WRONLY | O_CREAT, 06666);
	printf("fd2 : %d\\n", fd2);
	int fd3 = open("log.txt", O_WRONLY | O_CREAT, 06666);
	printf("fd3 : %d\\n", fd3);
	int fd4 = open("log.txt", O_WRONLY | O_CREAT, 06666);
	printf("fd4 : %d\\n", fd4);

	return 0;

四个文件描述符的值如下。

所以文件描述符的分配规则是:从最小的但未被使用的开始分配。以上面为例,0在一开始就被关闭,且是最小的,所以给fd1分配0,1和2都已经被占用,所以不能分配,3之后都没有被占用,所以从小到大依次分配。


四、重定向

1.输出重定向

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()

	close(1);//关闭标准输出
	umask(0);

	//由上面分配规则可知,这里open的返回值一定是1,即fd=1
	int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
	if (fd < 0)
	
		perror("open");
		return 1;
	

	printf("hello printf\\n");
	fprintf(stdout, "hello fprintf\\n");
	fputs("hello fputs\\n", stdout);
	fflush(stdout);//需要刷新才能看到结果

	close(fd);

	return 0;

结果如下:

代码一开始关闭了文件描述符为1的文件,即关闭了显示器,切断了1和stdout之间的联系。而printf以及fprintf和puts都向stdout这一FILE*的指针输入,在系统调用时只看1,而不管1是与stdout对应还是与其他文件对应,在上面的代码中,1与log.txt对应,所以所有向屏幕的输出都输入到了log.txt,也即重定向到了log.txt。

这里在各种打印结束后需要刷新stdout,因为向文件重定向时变成了全缓冲(下面会提到),如果不刷新就必须到缓冲区写满才会刷新,所以需要刷新stdout。


2.再谈缓冲区

【Linux小练习】进度条程序 中简单介绍了缓冲区,这里再深入地讲一下缓冲区。

(1)缓冲方式

  1. 无缓冲
  2. 全缓冲:多用于(磁盘)文件写入时。
  3. 行缓冲:常见于对显示器进行刷新时。

缓冲就像送快递一样,无缓冲是拿到一个快递就送一个快递,全缓冲是拿到所有快递后一次送完,行缓冲是拿到一定数量的快递就送一批。显然全缓冲从送快递的人的角度来看效率最高。

要刷新的数据就像快递,送快递就是将内容从缓冲区写到文件中。由于磁盘文件、显示器等都是外设,写入的效率很低,所以采用全缓冲来提高一些效率。但向显示器刷新时,显然我们都希望尽快从显示器得到结果,但不缓冲的效率太低了、行缓冲打印内容又不及时,所以折中采用行缓冲的方式。


(2)缓冲区

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()

	//C
	printf("hello printf\\n");
	fprintf(stdout, "hello fprintf\\n");
	
	//system
	const char* msg = "hello write\\n";
	write(1, msg, strlen(msg));

	fork();//在最后创建子进程
	return 0;


由上面的结果可得:

  1. 重定向与否会更改缓冲方式(向显示器打印是行缓冲,但向磁盘文件写入是全缓冲)。
  2. C语言的函数接口打印了两次,而系统接口只打印了一次。

上面现象的解释如下:

  1. 向显示器打印时,按行刷新,所以fork时缓冲区里的内容都已经打印完了(打印且刷新到显示器),创建子进程不会有影响。
  2. 但向磁盘文件(log.txt)重定向时,缓冲方式是全缓冲,当代码走到fork时,仅仅打印,但还没有刷新,当父子进程有一个刷新时,发生了写时拷贝,所以C语言接口打印的内容有两份。而write系统调用是没有缓冲区的,所以只会打印一次。

由此可知,所谓的缓冲区其实是语言自带的(C语言中的缓冲区在FILE结构体中维护),而系统并没有缓冲区。

下面是C语言FILE结构体中与缓冲区相关的内容

 //缓冲区相关
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */

3.输入重定向

输入重定向道理与输出重定向相同,就是关闭文件描述符0,然后通过分配规则将0赋给一个文件,从stdin中读入时就变成了从该文件中读。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>万字详解linux系列进程控制(代码片段)

文章目录一、环境变量1.基本概念2.常见的环境变量(1)PATH(2)HOME(3)SHELL(4)HISTSIZE(5)SSH_TTY3.与环境变量相关的指令4.在代码中获取环境变量(1)argc和argv(2&#x 查看详情

万字详解linux系列进程信号(代码片段)

文章目录一、信号简介1.查看信号2.信号的本质3.信号的记录和发送4.从键盘输入的信号5.signal自定义信号6.处理信号的一般方式二、信号产生1.通过终端按键(键盘)产生信号CoreDump(核心转储)2.程序异常事后调试3.调用函数&... 查看详情

万字详解linux系列进程间通信(ipc)(代码片段)

文章目录一、进程间通信1.目的2.如何通信3.分类二、管道1.概念2.匿名管道(1)实现父子进程间通信(2)fork角度的理解(3)文件描述符角度的理解(4)匿名管道特点(5)四种情况(6&#x... 查看详情

万字详解linux系列文件系统动静态库(代码片段)

文章目录一、文件系统1.inode2.磁盘3.目录4.软硬链接(1)软链接(2)硬链接5.文件的三个时间信息二、动静态库1.基本原理2.认识库3.动静态加载的优缺点静态加载动态加载感谢阅读,如有错误请批评指正一、文... 查看详情

linux进阶|万字详解docker镜像的制作,手把手学会!(代码片段)

  创作不易,来了的客官点点关注,收藏,订阅一键三连❤😜  前言运维之基础——Linux。我是一个即将毕业的大学生,超超。如果你也在学习Linux,不妨跟着萌新超超一起学习Linux,拿下Linux,一... 查看详情

万字详解linux系列多线程(上)(代码片段)

...容过多,所以分为两篇来写,下篇传送门:【万字详解Linux系列】多线程(下)。一、线程1.概念在一个程序里的一个执行路线就叫做线程(thread)。更 查看详情

万字详解linux系列多线程(下)(代码片段)

...指正前言由于多线程部分内容过多,所以本文接着【万字详解Linux系列】多线程(上)向后介绍多线程相关的内容。一、线程同步1.概念在保证数据安全的前提下,让线程按照某种特定的顺序访问临界资源,从... 查看详情

linux从青铜到王者第十三篇:linux多线程四万字详解(代码片段)

系列文章目录文章目录系列文章目录前言一、Linux线程概念1.什么是线程2.线程的优点3.线程的缺点4.线程的异常5.线程的用途二、进程和线程的对比1.进程和线程2.多进程的应用场景有哪些?三、线程控制1.POSIX线程库2.创建线程... 查看详情

万字详解linux常用指令(值得收藏)(代码片段)

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇本文将给大家详细介绍Linux常用的指令、演示以及一些基础知识的讲解目录ls指令file指令pwd命令whoami指令cd指令相对路径和绝对路径which指令touch指令mkdi... 查看详情

万字长文超硬核详细学习系列——深入浅出linux基础篇的知识点,值得你收藏学习必备(代码片段)

茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!愿你在未来的日子,保持热爱,奔赴山海!Linux基础篇目录1.Linux系统介绍1.1linux的概述1.2linux的优势1.3linux的分类1.4常见的发行版linux... 查看详情

万字长文详解yolov1-v5系列模型(代码片段)

一,YOLOv1Abstract1.Introduction2.UnifiedDetectron2.1.NetworkDesign2.2Training2.4.Inferences4.1ComparisontoOtherReal-TimeSystems5,代码实现思考二,YOLOv2摘要YOLOv2的改进1,中心坐标位置预测的改进2,1个gird只能对应一个目标的改进3,backbone的改进4, 查看详情

python基础2万字-详解python基础函数,包教包会(代码片段)

👉跳转文末👈获取作者联系方式,共同学习进步文章目录运行环境输入输出函数print()input()获取数据类型type()isintance()字符串操作str()eval()str.capitalize()str.center()str.count()str.find()&str.rfind()str.index()&str.rind 查看详情

linux进程详解——万字总结,复习利器(代码片段)

这里写目录标题一、图解计算机体系结构(1)底层硬件(2)驱动程序(3)操作系统(4)SystemCall(系统调用)(5)用户操作接口(6)用户层二、进程1、什么是进程2、创建... 查看详情

linux系列万字总结--centos第五天运行级别,找回密码,常用文件目录指令(代码片段)

Linux基础,万字总结,创作不易,希望能够支持小舞鸭1.指定运行级别1.1指定运行级别的说明2.如何找回root密码(经典面试题)3.帮助指令3.1man指令3.2help指令4.常用文件目录指令4.1常用文件目录指令(一路径... 查看详情

[os-linux]详解linux的基础io-------文件描述符fd(代码片段)

 本文由文件IO相关操作的一些操作,进一步详解了文件描述符fd,重定向,FILE结构体。目录一、C语言中的文件I/O操作二、系统文件I/O1.接口介绍2.open函数返回值三、文件描述符fd四、文件描述符的分配规则六、dup2系... 查看详情

[os-linux]详解linux的基础io-------文件描述符fd(代码片段)

 本文由文件IO相关操作的一些操作,进一步详解了文件描述符fd,重定向,FILE结构体。目录一、C语言中的文件I/O操作二、系统文件I/O1.接口介绍2.open函数返回值三、文件描述符fd四、文件描述符的分配规则六、dup2系... 查看详情

[python从零到壹]十七.可视化分析之matplotlibpandasecharts入门万字详解(代码片段)

欢迎大家来到“Python从零到壹”,在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界。所有文章都将结合案例、代码和作者的经验讲解,真心想把自己近十年的编程经验分... 查看详情

linux进程信号万字详解(上)(代码片段)

🎇Linux:博客主页:一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话:看似不起波澜的日复一日,一定会在某一天... 查看详情