上篇文章:Linux操作系统2-进程控制3(进程替换,exec相关函数和系统调用)_execv系统调用-CSDN博客
本篇代码Gitee仓库:myLerningCode · 橘子真甜/linux学习 - 码云 - 开源中国 (gitee.com)
本篇重点:C语言基础IO与系统调用
目录
[一. 文件相关知识](#一. 文件相关知识)
[二. C语言的文件操作](#二. C语言的文件操作)
[2.1 fopen](#2.1 fopen)
[2.2 fclose](#2.2 fclose)
[2.3 fread](#2.3 fread)
[2.4 fwrite](#2.4 fwrite)
[2.5 fprintf](#2.5 fprintf)
[2.6 举例代码](#2.6 举例代码)
[三. 文件相关的IO系统调用](#三. 文件相关的IO系统调用)
[3.1 open](#3.1 open)
[3.2 close](#3.2 close)
[3.3 write](#3.3 write)
[3.4 read](#3.4 read)
[3.5 举例代码操作](#3.5 举例代码操作)
[四. OS是如何管理被打开的文件?](#四. OS是如何管理被打开的文件?)
[4.1 文件fd](#4.1 文件fd)
[五. 下篇重点: 文件fd, Linux下一切皆文件](#五. 下篇重点: 文件fd, Linux下一切皆文件)
一. 文件相关知识
在基础指令这篇文章 Linux基础1-基本指令2(你真的了解文件吗?)-CSDN博客 中,我们提到了文件的相关命令。总结一下
1 一个空文件也需要占用空间
2 文件 = 文件内容 + 文件属性
3 文件操作 = 操作文件内容 + 操作文件属性
4 我们使用文件路径+文件名标记一个文件
5 进程想要访问一个文件必须要先通过OS打开这个文件
C语言为用户提供了文件操作,C++也有相关的文件操作。像这些语言级别的库函数提供的文件操作,**都是对OS提供的文件操作系统调用的封装。**所以,学习系统调用提供的文件操作有利于我们掌握语言级的文件操作
二. C语言的文件操作
C语言中的库函数为我们提供了很多操作文件的函数:fopen,fclose,fwrite,fread,fprintf,fscanf...等。
2.1 fopen
fopen用于打开一个文件,其函数原型如下:
//所需头文件
#include <stdio.h>
//函数原型
FILE* fopen(const char* filename, const char* mode);
//filename,打开文件的路径。直接写名字默认在当前路径下查找
//mode,打开的方式
"r" 只读方式打开
"w" 只写方式打开,默认会清空文件中的内容
"a" 只写,写的方式是追加
"b" 以二进制方式打开,一般配合r和w使用
"W+" 读写,没有文件会创建,写方式是清空文件从头开始写
"r+" 读写,从头开始写文件
"a+" 读写,追加写
2.2 fclose
用于关闭一个打开的文件
//函数原型
int fclose(FILE* stream);
//关闭stream这个文件流(被fopen打开的文件流)
2.3 fread
用于读一个文件中的数据
cpp
//函数原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr: 读取的文件存放在内存中的位置
size:读取文件中元素大小(以字节为单位)
nmemb:读取文件元素的数量
stream:读取文件的文件流(你要读取的文件)
2.4 fwrite
cpp
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
//参数和fread类似
//只是功能是将ptr写入到stream这个文件流中
2.5 fprintf
cpp
//函数原型
int fprintf(FILE *stream, const char *format, ...);
//用法和printf一样,不过是将数据写入到stream这个文件流中
其他文件操作都和上述文件操作类似。具体内容可以查找man手册
2.6 举例代码
用一段代码来举例这些操作的用法
test.c
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define MY_FILENAME "log.txt"
int main()
{
// 1.写入数据
FILE *fp = fopen(MY_FILENAME, "w");
if (fp == NULL)
{
// 打开文件失败
perror("fopen");
}
// 2.使用fwrite写数据,写入三行 Hello world
const char *buffer = "Hello World!\n";
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
// 3. 关闭文件
fclose(fp);
fp = NULL;
return 0;
}
Makefile
cpp
test:test.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -rf test log.txt
测试结果如下:
修改 log.txt 和 test.c 进行读取数据
log.txt
cpp
Hello World!
Hello World!
Hello World!
YZC yzc
abc 123
156 1sg 45qe1r 5h@#@ ^% 56 @# ^re8 5qh qer56h 16 32`7 tr314yt 9bm v891-3
test.c
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define MY_FILENAME "log.txt"
int main()
{
// 1.读取数据
FILE *fp = fopen(MY_FILENAME, "r");
if (fp == NULL)
{
// 打开文件失败
perror("fopen");
}
// 2.使用fread写数据,写入三行 Hello world
char buffer[200];
fread(buffer, sizeof(char), 200, fp);
buffer[strlen(buffer) - 1] = '\0'; //将最后的'\n'变为'\0'
printf("%s\n", buffer);
// 3. 关闭文件
fclose(fp);
fp = NULL;
return 0;
}
测试结果:
注意C语言的字符串默认在结尾有一个'0',而文本文件中末尾并没有'\0'。所以我们使用C语言接口读取文件后,如果是字符串,需要在末尾加上'\0'
三. 文件相关的IO系统调用
3.1 open
打开文件的系统调用
cpp
//所需头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//函数原型
int open(const char *pathname, int flags) //用于打开已经创建的文件
int open(const char *pathname, int flags, mode_t mode) //用于打开和创建文件
//pathname 打开文件的名字
//flags 打开文件的方式
//mode 创建文件时候文件的权限
//常见的flag
O_RDONLY 表示只读
O_WRONLY 只写
O_WRONLY 读写
O_APPEND 追加写
O_CREAT 没有这个文件要创建文件
O_TRUNC 打开文件的时候清空文件内容
3.2 close
关闭文件fd的系统调用
cpp
//文件关闭
#include <unistd.h>
//函数原型
int close(int fildes);
3.3 write
向文件写入数据
cpp
//头文件
#include <unistd.h>
//函数原型
ssize_t write(int fd, const void *buf, size_t count);
//fd 写入的文件fd
//buf 要写的数据缓冲区来源
//count 写入的字节个数
//返回值,成功写入,返回写入的字符数,失败返回-1
buf是void* 的原因:在系统看来,任何数据都是二进制
3.4 read
从文件中读取数据
cpp
//头文件
#include <unistd.h>
//函数原型
ssize_t read(int fd, void *buf, size_t count)
//将fd文件中的count字节数量的数据读取到buf中
//返回0表示读取到文件结尾
3.5 举例代码操作
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MY_FILENAME "log.txt"
int main()
{
// 1.打开文件,方式是读写,没有文件创建,清空文件从头开始写。创建的文件权限是0666
umask(0); // 清空系统的umask
int fd1 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
// 如果写入失败
if (fd1 < 0)
{
perror("open");
return -1;
}
// 2.写入数据
char buffer[64];
int cnt = 5;
while (cnt)
{
sprintf(buffer, "YZC Hello World [%d]\n", cnt--); // 将数据写入缓冲区
write(fd1, buffer, strlen(buffer)); // 向文件写入数据不需要添加'\0'
}
// 3.关闭文件描述符fd
close(fd1);
// 4.读取这些数据
int fd2 = open(MY_FILENAME, O_RDONLY);
// 如果文件打开错误
if (fd2 < 0)
{
perror("open");
return -1;
}
// 读取文件的时候,buffer最多读取sizeof(buf)个数据,由于有'\0'。所以要-1
char *buf[64];
ssize_t num = read(fd2, buf, sizeof(buf) - 1);
printf("%s", buf);
// 关闭文件
close(fd2);
return 0;
}
测试结果:
语言级别的IO操作库函数都是对系统调用IO操作的封装
四. OS是如何管理被打开的文件?
我们知道,OS通过PCB来管理进程。在OS中有很多进程,这些进程也会打开很多的文件。那么OS是如何管理这些被打开的文件的?
OS为了管理被打开的文件,创建对应的内核数据结构 struct_file。这个结构体包含了文件的大量属性。
4.1 文件fd
文件fd是什么东西?我们打印出来看看
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MY_FILENAME "log.txt"
int main()
{
umask(0); //清楚umask码,仅仅修改该进程创建的文件
int fd1 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd5 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
printf("fd4:%d\n",fd4);
printf("fd5:%d\n",fd5);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
return 0;
}
fd为什么从3开始?
因为C语言会默认打开三个输入输出流,stdin, stdout, stderr。
即标准输入,标准输出,标准错误。它们占用了0 1 2
通过stdin这个文件的结构体中的 _fileno 即可获取fd
cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MY_FILENAME "log.txt"
int main()
{
umask(0); //清楚umask码,仅仅修改该进程创建的文件
int fd1 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd5 = open(MY_FILENAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("stdin->fd [%d]\n",stdin->_fileno);
printf("stdout->fd [%d]\n",stdout->_fileno);
printf("stderr->fd [%d]\n",stderr->_fileno);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
printf("fd4:%d\n",fd4);
printf("fd5:%d\n",fd5);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
return 0;
}
测试结果如下:
这些数字其实是一个数字的下标,在PCB中有一个指针数组 (称为文件描述符表)。这个指针数组存放的是指向struct_file这个文件管理的内核数据结构。
进程通过fd这个数组下标就能够访问文件结构体!
具体关系可见下图: