Linux----基础IO

文章目录

复习C语言io知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<stdio.h>
int main()
{
//FILE* fp=fopen("./log.txt","r");
FILE* fp=fopen("./log.txt","a");//追加,不会覆盖掉
if(NULL==fp)
{
perror("fopen");
return 1;

}
#if
// char buffer[32];
// while(fgets(buffer,sizeof(buffer),fp)!=NULL)
// {
// printf("%s", buffer);
// }
// if(!feof(fp))
// {
// printf("fgets quit not normal\n");
//
// }
// else
// {
// printf("fgets quit normal\n");
// }

int cnt=10;
const char* msg="hello ";
while(cnt--){
fputs(msg,fp);
}
fclose(fp);
return 0;
}

C 程序默认会打开3个输出流,stdin,stdout,stderr
stdin对应键盘,stdout对应显示器,stderr对应显示器

fputs(msg,stdout);//直接向显示器去写入

stdout是向显示器去输出
将原本应该显示到显示屏的内容,显示到了文件里面
本质是指把stdout的内容重定向到文件中
在这里插入图片描述

把原本应该打印在文件里面的打印在了显示器里面
在这里插入图片描述
fputs向一般文件或者硬件设备都能写入
磁盘也是硬件
同理
c++:cin,cout cerr

  1. c语言的一切操作实际上都是在向硬件写入(所有语言上对“文件”的操作都要贯穿操作系统)
    即最终都是访问硬件,
    用户行为–>语言,程序,lib–>OS–>驱动–>硬件

  2. 但是操作系统不相信任何人 ,所以访问操作系统是需要系统调用接口的
    所以几乎所有的语言fopen,fclose,fread等等的底层一定是使用OS提供的系统调用

学习文件的系统调用接口

离OS更近,更能了解

文件打开

1
2
3
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

pathname就是要打开的路径名,flags就是我们打开的方式,mode就是打开的权限信息,
会返回一个文件描述符,int类型
flag是整数,:传递标志位,

int:32个bit位,一个bit,就代表一个标志,就代表一个标志位
0000 0000,如以最后一个标志位为0还是1,代表是读还是写
,以第一个标志位,代表是否创建文件
可以传多个,还快

1
if(O_WRONLY&flag)

判断结果,所以O_WRONLY O_rdnly O_CREAT
这些都是只有一个比特位为1的数据,而且都不重复

#define O_WRONLY 0x1
#define O_RDONLY 0x2
等等

如果想要得到两个以上的功能,我们直接|就可以了

这就是通过比特位的方式传多组标记的做法

文件关闭
在这里插入图片描述

1
2
3
4
5
6
7
8
int fd=open("./lg.txt",O_WRONLY|O_CREAT);//以只写方式打开,如果文件不存在,就会帮助我们创建一个
//相当于C语言中的w选项,
if(fd<0)
{
//打开文件失败
printf("open err\n");
}
close(fd);//文件就关掉了

我们没有输入第三个参数(权限参数),那么加入不存在这个文件,那么产生的文件的权限是乱的
在这里插入图片描述

文件描述符

1
2
3
4
5
6
7
8
9
10
int fd=open("./g.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位
//以只写方式打开,如果文件不存在,就会帮助我们创建一个
//相当于C语言中的w选项,0644以二进制的方式显示权限
if(fd<0)
{
//打开文件失败
printf("open err\n");
}
printf("fd:%d\n",fd);
close(fd);//文件就关掉了

在这里插入图片描述

1
2
3
4
5
int fd=open("./g.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位
int fd1=open("./g1.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位
int fd2=open("./g2.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位
int fd3=open("./g3.txt",O_WRONLY|O_CREAT,0644);//一次传递两个标志位
//以只写方式打开,如果文件不存在,就会帮助我们创建一个

在这里插入图片描述
我们发现是从3开始连续

文件描述符那么0,1,2去那里了呢

0:标准输入,键盘
1:标准输出,显示器
2:标准错误,显示器

0 1 2 3 4 5 6 7
我们可以联想到一个数组的下标

open的返回值是OS系统给我们的

文件与进程

所有的文件操作,表现上都是进程执行对应的函数

进程对文件的操作

  1. 要操作文件必须先打开文件
  2. 打开文件的本质:将文件相关的属性信息加载到内存
  3. 系统中会存在大量的进程,进程可以打开多个文件,系统中存在更多的打开的文件
    那么OS 要把打开的文件管理起来,
    (先描述再组织)
  • 如果一个文件没有被打开,没有被创建,那么这个文件就在磁盘上,同理一个进程没有被打开,这个进程也在磁盘上面
  • 如果创建了空文件(内容),要不要占磁盘空间呢,但是还有文件的属性,也是数据,,所以也是要占据磁盘空间,
    磁盘文件=文件内容+文件的属性

对文件的操作:

  1. 对文件内容进行操作
  2. 对文件属性进行操作

管理的思路
先描述再组织

1
2
3
4
5
struct file
{
//包含了打开文件的相关属性信息
//链接属性
}

在进程里面有
struct task_struct
{
struct files_struct* fs;//地址,指向的就是其对应的内容
}

1
2
3
4
struct files_strtuct
{
struct file* fd_array[];//指针数组
}

因为array是一个指针数组,所以可以用对应的下标找到对应的地址0,1,2
相当于fd_array[0]就指向了一个文件

所以0,1,2分别被标准输入,标准输出,标准错误文件给占用
每次生成一个文件,再内存里面就要形成一个struct file结构,在把地址填入到array下标处

而我们再write和read的时候,都要传入fd

  1. 执行write和read调用的是进程,而进程就能通过自己的PCB 找到对应的fs指针,找到files_struct里面根据文件描述符找到对应的文件,进行相关操作

fd

本质就是内核中进程和文件关联的数组的下标

一切皆文件

一切皆为文件
每一个硬件都有其对应的write和read的方法
在这里插入图片描述

虚拟文件,可以类比于多态,使用函数指针,就用指针调用对应的函数,这些函数调用这些硬件对应的方法,
多态就是实现一切皆()的高级方法
在这里插入图片描述
如我们在上层调用read/write的时候,就指向了对应的fd,再指向其对应的方法
在这里插入图片描述

1
2
3
4
5
6
7
void Fd_Dewrite()
{
const char* msg="hello";
write(1,msg,strlen(msg));//向标准输出去写入
write(1,msg,strlen(msg));//向标准输出去写入
write(1,msg,strlen(msg));//向标准输出去写入
}

我们直接向标准输出去书出
在这里插入图片描述

直接从键盘输入

1
2
read(0,buf,sizeof(buf)-1);//直接从键盘上写入
printf("echo :%s",buf);

在这里插入图片描述

文件描述符的分配规则

1
2
3
4
5
6
7
8
void  Fd_Alloc_Base()
{
close(0);
close(2);//把0对应的标准输入给关闭
int fd=open("./g.txt",O_CREAT|O_WRONLY,0644);
printf("fd=%d \n",fd);
close(fd);
}

在这里插入图片描述
分配规则

每次给新文件分配的fd,是从fd_array中找到最小的,没有被使用的,作为新的fd

1
2
3
4
5
6
7
8
void  Fd_Alloc_Base()
{
close(1);
close(2);//把0对应的标准输入给关闭
int fd=open("./g.txt",O_CREAT|O_WRONLY,0644);
printf("fd=%d \n",fd);
close(fd);
}

我们把1的标准输出给关了,
在这里插入图片描述

没有显示到显示屏中,而是显示到了文件中
这就是输出重定向

原因:我们把1的文件描述符给关了,所以现在给g.txt里面的fd就是1,
而printf里面对应的文件的fd一定是1,所以现在g.txt的fd为=1,就写到了g.txt

而printf和fprintf里面的都是有相关的fd,因为操作系统最大,

write

功能:向文件描述符写入,在buf的用户缓冲区里面,期望写入cout个字节
return:返回的是实际向文件里面写入多少个字节的内容
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int fd=open("demo.txt",O_CREAT|O_WRONLY,0644);
if(fd<0)
{
perror("open");
exit(-1);
}
const char*msg="hello \n";
int cnt=5;
while(cnt)
{
write(fd,msg,strlen(msg));//我们写入文件的过程中,我们要不要加入\0呢,不需要,
//因为\0作为字符串的结束标志位,只是c的规定,而文件关心字符串的内容,
cnt--;
}

close(fd);
return 0;
}

我们用write写入了5个hello,
在这里插入图片描述

read

功能:从文件描述符中读取指定内容,一次读取到的内容,都放到用户层缓冲区中,每次读取count个字节
return:如果count是我们期望读多少个字节,返回就是我们实际读取多少个字节
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void FdRead()
{
int fd=open("./demo.txt",O_RDONLY);//以读的方式打开不涉及到创建,权限也不要写了
if(fd<0)
{
perror("open");
exit(-1);
}
char buff[1024];
ssize_t s=read(fd,buff,sizeof(buff)-1);//-1是因为我们不需要\0

if(s>0)
{
//说明我们读取到了有效的内容
//因为我们要把读取到的内容当作一个字符串看待,所以要在最结尾添加一个\0,作为字符串结束标志
buff[s]=0;
printf("%s\n",buff);
}
close(fd);

}

在这里插入图片描述

重定向

输出重定向

1
echo "hello world">log.txt

我们可以理解为把echo的1关掉,在把log打开,再把所有内容打印到里面

追加重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
void  Fd_Alloc_Base()
{
close(1);
// close(2);//把0对应的标准输入给关闭
int fd=open("./g.txt",O_CREAT|O_WRONLY|O_APPEND,0644);
printf("fd=%d \n",fd);
printf("hello world");
printf("hello world");
printf("hello world");
printf("hello world");
printf("hello world");
// close(fd);
}

只在open的时候把append选项加上去
输入重定向
就是把原来从键盘里面读入的东西,现在从一个文件里面读
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
void Fd_Redefout()
{
close(0);//把输入给关掉
int fd=open("./g.txt",O_RDONLY);
char line[128];
while(fgets(line,sizeof(line)-1,stdin))//因为现在g.txt的fd为0,所以stdin就是g.txt
{
//原本应该从键盘读取的内容,现在是从文件里面读取了
//输入重定向
printf("%s\n",line);
}

}

验证文件描述符

1
2
3
4
5
6
7
8
void Verify_IO()
{
printf("stdin-> %d",stdin->_fileno);
printf("stdout-> %d",stdout->_fileno);
printf("stderr-> %d",stderr->_fileno);

}

在这里插入图片描述

dup2

在这里插入图片描述
newfd是oldfd的一份拷贝,数组内容的拷贝,指针的拷贝
所以全部变成old,
输出重定向

1
dup2(fd,1)

shell中的重定向
echo “hello” > file.c
fork->child->dup2(fd,1)->exec(“echo,“echo”)

fork创建之后 子进程也有fd,而且文件描述符和父进程都一样
但是打开的那些文件不会新建,因为我们是在创建进程,

如果父进程打开了标准输入,输出,错误,子进程也会继承下去
因为bash是所有进程的父进程,而bash打开了标准输入,输出,错误,所以所有的子进程也都继承下去了

缓冲区

标准输出和标准错误

1
2
3
4
5
6
7
8
9
int main()
{
const char* msg="hello stdout\n";
write(1,msg,strlen(msg));
const char* msg2="hello stderr\n";
write(2,msg2,strlen(msg2));

return 0;
}

在这里插入图片描述
我们发现只有标准输出重定向到了文件里面,但是标准错误仍然打印在屏幕里面了

因为重定向只有fd=1的被弄进去,而fd=2不会被弄进去

1
./redir >log.txt 2>&1

把标准输出和标准错误都重定向进去

1
2
3
$ cat log.txt
hello stdout
hello stderr

./redir >log.txt 2>&1
先执行前面第一条语句,此时把fd=1原本指向显示器改为指向一个特定文件,而fd=2的文件原本指向了显示屏,把1里面的内容拷贝到2里面,所以2也指向1指向的文件

缓存区

1
2
3
4
5
6
7
8
9
10
11
int main()
{
const char* msg="hello stdout\n";
write(1,msg,strlen(msg));
const char* msg2="hello stderr\n";
write(2,msg2,strlen(msg2));
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
close(1);
return 0;
}

在这里插入图片描述
第一次可以显示出fprintf
close后,第二次不能输出这些内容

1
2
3
4
5
6
7
8
9
10
11
12
13
void  Fd_Alloc_Base()
{
close(1);
// close(2);//把0对应的标准输入给关闭
int fd=open("./g.txt",O_CREAT|O_WRONLY|O_APPEND,0644);
printf("fd=%d \n",fd);
printf("hello world");
printf("hello world");
printf("hello world");
printf("hello world");
printf("hello world");
// close(fd);
}

把fd关闭之后就没有显示内容了

c语言本身也
我们曾经说的缓冲区都是用户级缓冲区,都是语言层面
printf是向stdout写入(FILE*)–》(struct file)
我们使用printf和fprintf,我们并没有写到OS里面,而是写到c语言缓冲区,把c语言的缓存区刷新到操作系统,

在这里插入图片描述
我们在特定的情况下才会把数据刷新到内核缓冲区,

  1. 遇到\n的时候,会刷新到显示器上面,
  2. 进程退出的时候,会刷新FILE 内部的数据到OS,没有进程退出的时候数据还会在C缓冲区里面,

用户——>OS

刷新策略

  1. 立即刷新(不缓冲)
  2. 行刷新(行缓冲\n):如显示器打印,
  3. 全缓冲区满了才刷新(也就是不会溢出),比如,往磁盘文件里面写入

OS—> 硬件,也是同样使用的

如果发生了重定向,
显示器 --> log.txt

原本是行刷新(行缓冲),现在就变成了全缓冲

1
2
3
4
5
6
7
8
9
10
11
12
13
void  Fd_Alloc_Base()
{
close(1);
// close(2);//把0对应的标准输入给关闭
int fd=open("./g.txt",O_CREAT|O_WRONLY|O_APPEND,0644);
printf("fd=%d \n",fd);
printf("hello world");
printf("hello world");
printf("hello world");
printf("hello world");
printf("hello world");
// close(fd);
}

对于这个代码

如果不close的话,所有的信息都打印到了文件当中

没有close的话,所有的消息都直接输出到了c语言缓冲区里面,然后因为没有close掉fd,这批数据就会在进程退出的时候,数据就会刷新到内核,

把close放开,文件里面什么都没有,

所有的消息,刷新变成了全缓冲,有可能并没有被写满,说明可能没有立即被刷新到文件里头,而进程退出之前调用了close,就把文件描述符给关了,进程退出的时候数据还在缓冲区里面,来不及刷新到内核当中,所以文件里面就没有看到对应的内容

如果把close(1)关掉,那么printf把数据都刷新到用户缓冲区里头,行刷新,立马刷新了

如果在close之前
fflush(stdout),强制刷新缓冲区里面,就可以了写到文件里面了,

FILE*里面是有包含缓冲区的

1
2
3
4
5
6
7
8
9
10
11
int main()
{
const char *msg = "hello stdout\n";
write(1, msg, strlen(msg));
const char *msg2 = "hello stderr\n";
write(2, msg2, strlen(msg2));
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
close(1);
return 0;
}

在这里插入图片描述
我们发现我们即使close(1),仍然能够打印在显示屏上,因为三行刷新,有\n就刷新了

我们把内容重定向到文件里面,
发现只有write的1被写进去了

在这里插入图片描述
在这里插入图片描述
这是因为我们close(1),当重定向的时候原本要写到1里面的内容,显示到文件中,就从行刷新,变成了全缓冲,不立即刷新,而write是系统调用没有经过c语言缓冲区,就可以打印到文件里面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
const char *msg = "hello stdout\n";
write(1, msg, strlen(msg));
// const char *msg2 = "hello stderr\n";
// write(2, msg2, strlen(msg2));
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fputs("hello puts\n",stdout);
// close(1);

fork();
return 0;
}

在这里插入图片描述
我们如果2往显示器上打印,大家都正常,如果往文件里面打印,c接口会重复,系统接口不受影响
刷新策略变了,
写入文件里面的时候,就变成了全缓冲,数据就先写到了c语言缓冲区里面(不是操作系统提供的),

fork之后发生了写时拷贝,父进程写到了缓冲区里面,子进程也刷新到了缓冲区里面,当副进程退出的时候,就把数据i刷新出去,子进程也要刷新,所以因为写实拷贝的问题,出现了重复刷新

而如果在fork之前就把数据全部fflush出去的话,fork之后就不会发生写实拷贝,因为缓冲区里面没有数据了,可是write没有打印两个,


Linux----基础IO
http://example.com/2022/04/14/Linux----基础IO/
作者
Zevin
发布于
2022年4月14日
许可协议