【校招 --阶段一 系统编程】基础IO

41 篇文章 1 订阅
本文详细解析了C语言中文件操作的C接口与系统接口,包括fopen/fread/fwrite与open/read/write的使用,重点讲解了文件描述符fd的分配规则、缓冲区机制,以及如何通过dup2实现重定向。同时揭示了printf/fprintf与write的输出差异以及它们与缓冲区的关系。
摘要由CSDN通过智能技术生成

先来段代码回顾C文件接口
C 文件写入

#include <stdio.h>
#include <string.h>
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);
}
fclose(fp);
return 0;
}

C文件读出

#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "r");
if(!fp){
printf("fopen error!\n");
}
char buf[1024];
const char *msg = "hello bit!\n";

while(1){
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
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;
}

输出信息到显示器,你有哪些方法

#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}

stdin & stdout & stderr

C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

系统文件I/O

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:

文件写入:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello bit!\n";
int len = strlen(msg);
while(count--){
write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数
据。 返回值:实际写了多少字节数据
}
close(fd);
return 0;
}

文件读出:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello bit!\n";
char buf[1024];
while(1){
ssize_t s = read(fd, buf, strlen(msg));//类比write
if(s > 0){
printf("%s", buf);
}else{
break;
}
}
close(fd);
return 0;
}

接口介绍

#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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1

同时打开几个文件测试一下:

#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;
int main(){
int fp=open("fp.txt",O_RDONLY);
int fp1=open("fp1.txt",O_RDONLY | O_CREAT );
int fp2=open("fp2.txt",O_RDONLY | O_CREAT);
if(fp<0){
cout<<"open fail"<<endl;
}
char buff[256];//读入缓冲区
read(fp,buff,256);
cout<<"fd:"<<fp<<endl;
cout<<"fd1:"<<fp1<<endl;
cout<<"fd2:"<<fp2<<endl;
close(fp);
return 0;
}

在这里插入图片描述
问题:

  • 1 fd为什么是从3开始,之后开始连续的呢?
    是因为fd为文件描述符,在os层面就是一个整数,从0~n。因为0:标准输入,1:标准输出,2:标准错误,所对应的是stdin,stdout,stderr。
    0,1,2,3…本质就是数组下标
  • 2 进程是怎样和文件相关联的?
    系统中存在大量被打开的文件,os一定要把这些文件管理起来

    os将文件链表连接起来

os中进程也是这样被用链表连接起来的

在这里插入图片描述
而进程可以打开多个文件所以进程:文件=1:n那么进程PCB是怎样和文件关联起来的,LINUX内核用的是数组的一个东西

在这里插入图片描述
结构体指针数组数组前3个是默认打开的文件,接着数组后边是指向的是进程主动打开的文件。当进程打开一个文件先将它,先将未使用从小的文件描述符分配给文件,接着再链入到os管理的文件打开列表。文件描述符本质就是数组下表。
对于进程来说,默认的文件描述符从3开始,可以从最小的未被使用的分配fd给进程。

open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用和库函数

上面的fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图

在这里插入图片描述
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

文件描述符fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:

#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,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符的分配规则

#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,在看

#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数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

重定向

看代码:

#include<iostream>
#include<stdio.h>

#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;
int main(){
//close(1);
int fp=open("fd.txt",O_WRONLY | O_APPEND | O_CREAT);
//int fp1=open("fp1.txt",O_RDONLY | O_CREAT );
//int fp2=open("fp2.txt",O_RDONLY | O_CREAT);
if(fp<0){
cout<<"open fail"<<endl;
}
char str[]="hello word!\n";
char str1[]="hello China!\n";
char str2[]="hello shanxi\n";
write(1,str,strlen(str));
printf(str1);
fprintf(stdout,str2);
fflush(stdout);
close(fp);
return 0;
}

在这里插入图片描述
结果输出输出到stdout中如果关闭1呢?
在这里插入图片描述
stdout没有输出结果
在这里插入图片描述
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件fd.txt 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

那重定向的本质是什么呢?
在这里插入图片描述
当关闭1时,为什么printf和fprintt往显示器上打印的却显示到了文件中?
因为1是文件描述符,所以1和stdout相比更靠近底层,因为stdout是一个FILE*类型,而FILE是一个结构体,struct FILE结构体中包括一个fd的文件描述符。本来printf和fprintf是往stdout中写入的但是他的文件描述符号为1,而系统中文件描述符为1的重定向为其他文件所以在往stdout中写入时会写入到其他文件中

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

缓冲区:
显示器:行缓冲、
文件:全缓冲

重定向:重定向可能影响缓冲方式

FILE

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

先看一段代码:

#include<iostream>
#include<stdio.h>

#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;
int main(){
//close(1);
int fp=open("fd1.txt",O_WRONLY | O_APPEND | O_CREAT);
//int fp1=open("fp1.txt",O_RDONLY | O_CREAT );
//int fp2=open("fp2.txt",O_RDONLY | O_CREAT);
if(fp<0){
cout<<"open fail"<<endl;
}
char str[]="hello word!\n";
char str1[]="hello China!\n";
char str2[]="hello shanxi\n";
write(1,str,strlen(str));
printf(str1);
fprintf(stdout,str2);
fork();
fflush(stdout);
//fork();
close(fp);
return 0;
}

执行结果
在这里插入图片描述
当执行了close(1)结果如下:
在这里插入图片描述
我们发现printf 和fprintf (库函数)都输出了2次,而write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

当没有重定向时而,写入显示器是行缓冲缓冲区很快刷新,当执行到fork()时缓冲区已经刷新完毕,所以不会由fork创建的子进程和父进程两个进程刷新了。、

当发生重定向时,printf fprintf 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,直到fork之后,但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。fflush也是库函数,将由库函数自带的缓冲区中的数据刷新出来,由于fork创建了子进程,所以fflush要执行两编。
write 没有变化,说明没有所谓的缓冲。

printf fprintf 库函数会自带缓冲区,而write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fprintf 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write 没有缓冲区,而printf fprintf 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

使用 dup2 系统调用

#include <unistd.h>
int dup2(int oldfd, int newfd);

原理如下:
在这里插入图片描述
将文件描述符中的内容拷贝到文件描述符为1中让文件的文件描述符由3变成1

#include<iostream>
#include<stdio.h>

#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;
int main(){
//close(1);
int fp=open("fd2.txt",O_WRONLY | O_APPEND | O_CREAT);
//int fp1=open("fp1.txt",O_RDONLY | O_CREAT );
//int fp2=open("fp2.txt",O_RDONLY | O_CREAT);
if(fp<0){
cout<<"open fail"<<endl;
}
dup2(fp,1);
close(fp);
char str[]="hello word!\n";
char str1[]="hello China!\n";
char str2[]="hello shanxi\n";
write(1,str,strlen(str));
printf(str1);
fprintf(stdout,str2);
fork();
//fflush(stdout);
//fork();
close(fp);
return 0;
}

把本来输出到显示器中的数据输入文件中了。
在这里插入图片描述

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

自首的小偷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值