目录
前言
书接上文【Linux】进程控制(二)----进程程序替换、编写自主Shell命令行解释器(简易版)详情请点击查看,今天继续介绍【Linux】基础I/O----C语言文件操作与系统调用文件操作
一、文件知识补充
- Linux下一切皆文件(键盘、显示器、网卡、磁盘......),
文件 = 内容 + 属性(元数据),对文件的操作就是对文件内容或者文件属性做操作 - 访问一个文件,必须先将文件打开。访问文件就是对文件做增删查改,通过冯诺依曼体系我们可以知道CPU和内存打交道,不直接和输入、输入进行数据传输,因此访问文件时,要将文件加载到内存中
- 没有加载到内存中的文件,默认保存在磁盘中。我们对文件的学习就是学习
被打开的文件和未被打开的文件 - 对于0KB的空文件是占用磁盘空间的
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的;磁盘是外设(即是输出设备也是输入设备)
- 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,简称IO
- 用户通过bash,启动进程,让进程通过操作系统(只有操作系统才能访问到磁盘)将文件打开,因此打开文件底层一定有使用系统调用打开文件
- OS中,一定存在大量被打开的文件,因此操作系统需要管理这些打开的文件(先描述,再组织),一定存在一个结构体描述被打开的文件,如同PCB结构一样
- 进程有task_struct,进程也会有打开的文件,我们研究被打开文件本质 就是研究:
进程与文件的关系
二、回顾C文件接口
C语言接口打开文件
- 下面是使用C语言打开文件(fopen接口)的代码
cpp
#include <stdio.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp)
{
perror("fopen");
return 1;
}
fclose(fp);
return 0;
}

- fopen函数是C打开文件的接口,传入参数文件名、打开文件模式
- 打开文件模式为r、r+、w、w+、a、a+
- 当fopen函数调用成功返回一个FILE指针 (C语言提供的数据类型),失败返回NULL并设置错误码
- fclose函数
- 我们在上面代码基础上打印进程pid,并让进程死循环,监测进程
cpp
#include <stdio.h>
int main()
{
printf("我是一个进程, pid : %d\n", getpid());
FILE *fp = fopen("myfile", "w");
if(!fp)
{
perror("fopen");
return 1;
}
while(1)
{
sleep(1);
}
fclose(fp);
return 0;
}
- 使用
ls /proc/[进程id] -l命令查看当前正在运行进程的信息,我们可以看到该进程中有cwd,这就是为什么我们fopen的第一个参数只传入文件名,会自动在当前工作路径目录下(/home/gy/117/code/linux/lesson5)新建文件:/home/gy/117/code/linux/lesson5/myfile
- 因此我们更改当前进程的工作路径,那么新建只有文件名的文件就会新建到指定路径:
chdir
- 从下面截图可以看到cwd变为了
/home/gy/117,在/home/gy/117路径下也看到了我们创建的文件myfile
- 因此,
打开一个文件必须要文件路径+文件名
C语言接口写文件
- C语言写入接口有很多:
fwrite、fputs、fputc,我们这里以fwrite来进行写入
const void *ptr:传入的写入数据size_t size:写入时的基本单元size_t nmemb:写入几个基本单元
- 下面代码,我们向myfile文件中写入一个字符串,C语言字符串结尾都是以'\0'结尾的,我们在传入写入单元大小时需不需要
strlen() + 1呢? - 答案是
不需要,因此字符串以'\0'结尾是C语言的规定,如果写入到文本文件中,会乱码。所以不需要写入到文件中,当我们将数据从文件读取过来的时候,再在末尾加上'\0'即可
cpp
#include <stdio.h>
int main()
{
printf("我是一个进程, pid : %d\n", getpid());
FILE *fp = fopen("myfile", "w");
if(!fp)
{
perror("fopen");
return 1;
}
const char* str = "hello linux\n";
fwrite(str, strlen(str), 1, fp);
fclose(fp);
return 0;
}
我们发现:写入文件中,每次都是先将文件清空,再写入
w:以w方式打开,如果文件存在,会先清空文件内容再做写入;如果没有该文件,创建文件,再做写入a:以a方式打开,如果文件存在,会直接在文件末尾写入,不做清空;如果没有该文件,创建文件,再做写入- w+:以w+方式打开读和写,如果文件不存在,新建文件再读写(文件开头位置读写)
- a+:以a+方式打开读和写,如果文件不存在,新建文件,初始读的时候在文件的开始,写在文件末尾追加写入
r:以r方式打开,读操作,从文件开头处读- r+:以r+方式打开读和写,从文件开始
注意:一般文件读写不会同时打开,因为文件读写有读写位置。文件有文本文件和二进制文件,我们可以将文件看作一维数组,文件的读写位置就是数组下标:从文件的开头位置读写,就是从数组0下标开始,在文件末尾追加写入,就是从数组末尾写入
echo重定向
- 之前我们学习了echo 重定向和追加重定向到某个文件中
echo "123" > myfile:我们发现myfile中数据由hello linux -> 123,再次echo "123" > myfile,文件中依旧是123,> myfile文件直接被清空了。- echo在命令行输入之后,变为一个进程,重定向到myfile,一定要将文件打开(w方式打开)

echo "123" >> myfile:追加重定向(以a方式打开)

C语言接口读文件
- fread接口读取文件
- fread和fwrite的返回值比较特殊,返回值是读取或写入了几个基本单元大小
feof函数:检测是否读取到文件结尾
cpp
#include <stdio.h>
int main()
{
printf("我是一个进程, pid : %d\n", getpid());
FILE *fp = fopen("myfile", "r");
if(!fp)
{
perror("fopen");
return 1;
}
while(1)
{
char buffer[1024];
buffer[0] = 0; //将数组清空
size_t n = fread(buffer, 1, sizeof(buffer) - 1, fp);
if(n > 0)
{
buffer[n] = 0;
printf("%s", buffer);
}
if(feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}

- cat命令底层就是使用的读文件操作来实现的
三、stdin/stdout/stderr
补充知识
- 向显示器写入123,写入的是一个一个字符'1'、'2' 、'3',而不是写入的int类型的123。所以显示器是字符设备
- 通过键盘输入的也是一个一个字符'1'、'2' 、'3',而不是int类型123,所以键盘也是字符设备
- 这也是在C语言/C++中我们打印字符时要指明类型
printf("%d", x):是将整型变量x = 123,变为字符'1'、'2' 、'3',再使用int putchar(int c)将字符格式化打印在显示器上,所以printf函数是格式化输出 ;int a = 0; scanf("%d", &a):键盘输入的是字符'1'、'2' 、'3',scanf函数将在键盘中获得的字符转换为整型123,再将其保存在a的空间中,所以scanf函数是格式化输入 - 显示器、键盘是
文本文件,二进制文件就是不需要 进行格式化工作、能直接保存的文件
stdin&stdout&stderr
- 进程启动时,会默认打开三个输入输出流(就是打开3个文件),分别是stdin、stdout、stderr(全局),这三个流的类型都是
FILE* stdin:标准输入->键盘文件stdout:标准输出->显示器文件stderr:标准错误->显示器文件

为什么要默认打开stdin、stdout、stderr
- 一个进程被调度,就是要使用CPU资源进行计算的,因此进程就需要从键盘获得数据,计算完之后,输出计算结果,计算出错了还得有输出错误,所以进程要默认打开stdin、stdout、stderr
输出信息到显示器上方法
printf、fprintf函数

cpp
int main()
{
const char* s1 = "hello printf\n";
printf(s1);
const char* s2 = "hello fprintf\n";
fprintf(stdout, s2);
return 0;
}
fputs函数

cpp
int main()
{
const char* s1 = "hello printf\n";
printf(s1);
const char* s2 = "hello fprintf\n";
fprintf(stdout, s2);
const char* s3 = "hello fputs\n";
fputs(s3, stdout);
return 0;
}
fwrite函数
- 因为向显示器打印就是向stdout中写入,stdout也是文件,也就可以调用文件写入接口,stdout也是FILE*类型

cpp
int main()
{
const char* s1 = "hello printf\n";
printf(s1);
const char* s2 = "hello fprintf\n";
fprintf(stdout, s2);
const char* s3 = "hello fputs\n";
fputs(s3, stdout);
const char* s4 = "hello fwrite\n";
fwrite(s4, strlen(s4), 1, stdout);
return 0;
}

四、系统调用接口实现文件的打开、写入
open函数
- 实际上打开一个文件,由于文件在磁盘中保存,只有操作系统才能管理磁盘,打开磁盘中文件,因此打开文件底层需要系统调用,我们学习的C语言打开文件接口其底层都是调用的open函数
- 当打开文件存在是调用两个参数的open函数;当打开文件不存在时,调用三个参数的open函数,因此文件不存在需要先创建文件,创建文件就要这是文件权限(r、w、x)

- 打开方式(flags):
O_APPEND、O_CREAT、O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR、O_TRUNC - flags是一个整数,一个整数有32个比特位,
O_APPEND、O_CREAT...这些是宏,各自代表一个数字,每个数字都只有一个比特位是1
设计一个宏
- 我们实现一个版本显示函数,设置不同版本的宏
VERSION1 (1 << 0)、VERSION2 (1 << 1).....,通过函数ShowVersion传入宏来实现对应的显示
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define VERSION1 (1 << 0) // 1 000001
#define VERSION2 (1 << 1) // 2 000010
#define VERSION3 (1 << 2) // 4 000100
#define VERSION4 (1 << 3) // 8 001000
#define VERSION5 (1 << 4) // 16 010000
void ShowVersion(int flags)
{
if(flags & VERSION1)
printf("VERSION1\n");
if(flags & VERSION2)
printf("VERSION2\n");
if(flags & VERSION3)
printf("VERSION3\n");
if(flags & VERSION4)
printf("VERSION4\n");
if(flags & VERSION5)
printf("VERSION5\n");
}
int main()
{
ShowVersion(VERSION1); //显示版本
printf("---------------------------\n");
ShowVersion(VERSION1 | VERSION2); //显示版本
printf("---------------------------\n");
ShowVersion(VERSION1 | VERSION3 | VERSION4); //显示版本
printf("---------------------------\n");
return 0;
}
通过传入不同的标志位来实现不同的显示,传入多个宏(用 | 连接)实现多个标志位的显示
使用open函数创建一个文件
- 系统调用关闭文件函数close

- 我们在打开文件时,如果文件不存在就需要默认创建该文件,则需要传入宏
O_CREAT:表示创建文件
cpp
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT);
if(fd < 0)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
这样创建出来一个文件,但是该文件权限乱码,所以我们新建文件时,必须传入权限 ,这里将文件权限设置为0666
cpp
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
- 我们设置权限为666,但是为什么最终显示文件权限是664呢?
- 原因是我们Linux系统中设置了umask = 0002,umask会限制掉对应的权限,把Others的W权限去掉了
- 如果我们想要创建权限为666的文件,则需要我们重设umask,系统调用函数umask

cpp
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
close(fd);
return 0;
}

write函数
- 上面open函数和close函数,我们已经实现了创建打开文件、关闭文件,现在我们在打开文件后进行写入操作

cpp
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char* str = "1234567890\n";
write(fd, str, strlen(str));
close(fd);
return 0;
}

- 当log.txt文件中已经保存了1234567890数据时,我们再次向该文件写入"abc",可以看到,使用write系统调用,并没有像C语言接口一样,先清空文件中的数据,再写入;而是从文件开始写入

- 如果我们想要写入文件时先清空文件内容,再做写入,那么在打开文件时传入
O_TRUNC:打开文件时清空文件内容 open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666)就类似于C语言接口的fopen("log.txt", "w")- 如果要使用追加方式写文件,
open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666)中O_TRUNC(清空)改为O_APPEND,open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666)
cpp
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // 类似于fopen("log.txt", "w")
//int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char* str = "1234567890\n";
write(fd, str, strlen(str));
close(fd);
return 0;
}

















