目录
一、文件的概念
二、C语言文件操作回顾
三、使用系统调用进行文件I/O
1、系统调用open
1.1open接口介绍
1.2open形参中标记位flags的意义
1.3open的使用
2、系统调用write
2.1write接口介绍
2.2write的使用
3、系统调用read
3.1read接口介绍
3.2read的使用
四、文件描述符fd
1、文件描述符
1.1进程如何找到自己打开的文件
1.2文件描述符fd为什么从3开始
1.3文件描述符fd和FILE的区别
2、文件描述符的分配规则
五、重定向
1、重定向的本质
2、重定向接口dup2
2.1输出重定向
2.2追加重定向
2.3输入重定
六、Linux中一切皆文件
一、文件的概念
1、空文件,在磁盘中也会占据空间,因为文件=内容+属性;
2、文件路径+文件名具有唯一性,如果没有指明文件路径,默认在当前路径下进行文件访问;
3、文件操作的本质是进程和被打开文件的关系。
二、C语言文件操作回顾
#include <stdio.h>
#include <string.h>
#define FILE_NAME "log.txt"
int main()
{
//FILE* fp=fopen(FILE_NAME,"w");//r(只读,出错)w(只写,新建)r+(读写,出错)w+(读写,新建)a(追加)a+(读写追加)
FILE* fp=fopen(FILE_NAME,"r");//r(只读,出错)w(只写,新建)r+(读写,出错)w+(读写,新建)a(追加)a+(读写追加)
if(fp==NULL)
{
perror("fopen");
return 1;
}
//int cnt=5;
//while(cnt--)
//{
// fprintf(fp,"%d%s\n",1,"jly");//向文件中写入数据
//}
char buffer[60];
while(fgets(buffer,sizeof(buffer),fp)!=NULL)//遇到\n或num-1或文件末尾为一轮读取。
{
buffer[strlen(buffer)-1]='\0';//因为后面的puts会补个'\n',所以要先去除一个'\n'
puts(buffer);//将str指向的C字符串写入标准输出(stdout)并附加换行符('\n')
}
fclose(fp);
return 0;
}
C语言的文件操作可以看博主的这一篇文章:【C语言】文件相关函数详解
三、使用系统调用进行文件I/O
1、系统调用open
1.1open接口介绍
NAME
open, creat - open and possibly create a file or device
SYNOPSIS
#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);//用于处理文件比存在的情况
int creat(const char *pathname, mode_t mode);
RETURN VALUE
open() and creat() return the new file descriptor, or -1 if an error occurred (in which case, errno is set appro‐
priately).//open ()和 create ()返回新的文件描述符fd,如果出现错误,返回 -1(错误码被设置)
NOTES
O_RDONLY(只读), O_WRONLY(只写), and O_RDWR(读写)
O_CREAT(创建文件),O_TRUNC(清空文件),O_APPEND(追加)
1.2open形参中标记位flags的意义
参数flags是标记位的意思:标记位可以利用每个比特位只有01的特点,比特位之间通过按位或传递选项。(有点像单片机操纵寄存器的感觉)
//利用不同比特位打印对应结果
#include <stdio.h>
//只有一个比特位是1,其他位都是0
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FILE_NAME "log.txt"
void show(int flags)
{
if(flag&ONE) printf("one ");
if(flag&TWO) printf("TWO ");
if(flag&THREE) printf("three ");
if(flag&FOUR) printf("four ");
printf("\n");
}
int main()
{
show(ONE|TWO);//输出one two
show(ONE|TWO|THREE|FOUR);//输出one two three four
return 0;
}
标记位flags使用三个宏进行传参,他们分别是O_RDONLY(只读), O_WRONLY(只写), 和O_RDWR(读写),结合上方代码,这三个宏为了表示不同的文件读取方式,内部二进制1的位置绝对不相同。
1.3open的使用
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{
//umask(0);//将子进程的umask置0,后续创建的文件权限即为666
//当文件不存在时,open的可写并不会创建log.txt,需要按位或上O_CREAT,加上创建文件功能,最后一个参数是文件创建时的权限
int fd=open(FILE_NAME,O_WRONLY | O_CREAT,0666);//最终权限=666&~umask
if(fd<0)
{
perror("open");
return 1;
}
close(fd);//使用系统调用close关闭文件指针
return 0;
}
1、当文件不存在时,使用int open(const char *pathname, int flags, mode_t mode);
如果目标文件不存在,open并不会像fopen那样帮助使用者创建对应文件,需要按位或O_CREAT,加上创建文件功能。第三个参数是创建文件的默认权限,如果不写,系统创建的文件会是乱码。
2、当文件存在时,使用int open(const char *pathname, int flags);
2、系统调用write
2.1write接口介绍
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:写入个数 目标写入文件 缓冲区数据位置 个数
2.2write的使用
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{
//"w"==O_WRONLY | O_CREAT | O_TRUNC,0666清空式写入
int fd=open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
//"a"==O_WRONLY | O_CREAT | O_APPEND,0666追加
//int fd=open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);
if(fd<0)
{
perror("open");
return 1;
}
char outBuff[60];
int cnt=5;
while(cnt)
{
sprintf(outBuff,"%s:%d\n","jly",cnt--);
write(fd,outBuff,strlen(outBuff));//将字符写入文件不用让strlen+1写入\n,除非就像在文件中写入\n
}
close(fd);
return 0;
}
将字符写入文件中可以不用写入\n。
write再次对有文本内容的文件进行写入时,并不会和C语言文件操作中的"w"选项一样,先清空文本内容,而是直接覆盖,使用时可以加上O_TRUNC对文本先进行清空操作。
使用O_APPEND增加追加操作。
3、系统调用read
3.1read接口介绍
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:读取成功字节数 读取至缓冲区 要求读取字节
3.2read的使用
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{
int fd=open(FILE_NAME,O_RDONLY | O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
char buffer[100];
ssize_t num=read(fd,buffer,sizeof(buffer)-1);//留一个位置放\0
if(num>0)
{
buffer[num]='\0';//把\0补回去
}
printf("%s\n",buffer);
close(fd);
return 0;
}
四、文件描述符fd
1、文件描述符
1.1进程如何找到自己打开的文件
一个进程可以打开多个文件,操作系统为了管理这些打开的文件,需要为每个被打开的文件创建对应的内核数据结构标识文件(struct file{}),这个结构体中包含文件的大部分属性。
那么操作系统中被打开的文件这么多,进程是如何区分哪些文件是属于我的,哪些文件时非我的?那是因为进程的task_struct结构体中包含struct files_struct* files的结构体指针,这个指针指向struct files_struct这个结构体,这个结构体中存在一个叫做struct file* fd_array[]的结构体指针数组,数组中每个结构体指针指向对应被打开文件的struct file{}。
1.2文件描述符fd为什么从3开始
当我们在一个进程中用open打开多个文件时,通过打印fd的值,发现文件描述符fd从3开始分配。那0、1、2这三个文件描述符分配给谁了呢?
printf("stdin->fd:%d\n",stdin->_fileno);
printf("stdout->fd:%d\n",stdout->_fileno);
printf("stderr->fd:%d\n",stderr->_fileno);
0、1、2默认被这三个流占用,所以我们打开的文件的文件描述符fd从3开始。
1.3文件描述符fd和FILE的区别
FILE* fp=fopen(FILE_NAME,"w");
int fd=open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
系统调用访问文件必须使用文件描述符。而C语言中的FILE是一个结构体,这个结构体中包含了文件描述符。
2、文件描述符的分配规则
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);//关闭1
int fd=open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//新文件fd为1
if(fd<0)
{
perror("open");
return 1;
}
printf("%d\n",fd);//printf->stdout
fprintf(stdout,"%d\n",fd);//printf->stdout
fflush(stdout);//刷新缓冲区内容至文件log.txt中
close(fd);
return 0;
}
1、如果手动把文件描述符数组下标为1的fd关闭,那么后续用open打开一个新的文件,将会占用这个下标为1的位置。文件描述符的分配规则为:从小到大,按照顺序寻找最小的且没有被占用的fd位置。
2、fd为1的位置代表标准输出流,1被关闭,新文件被打开将会占用1。如果后续想要执行打印操作,文件内容将会被打印至这个新文件中。这是一种输出重定向。
五、重定向
1、重定向的本质
>输出重定向
>>追加
<输入重定向
重定向的本质:oldfd不变,在内核中更改newfd下标对应的struct file*的指针。
文件描述符的本质是数组下标,当系统运行时,默认已加载3个文件描述符,分别是标准输入(键盘)、标准输出(显示器)、标准错误(显示器),他们占据了数组0,1,2三个位置。
可以用close(1)手动关闭标准输出,再重新打开一个文件,这个新文件会占据数组为1的位置(标准输出)。如果后续使用printf或fprintf函数进行输出,数据将会输出重定向至这个新文件(因为这个新文件占据了标准输出)。
不过一般不会用手动关闭这三个文件描述符来达到重定向的效果,可以使用int dup2(int oldfd, int newfd);将文件拷贝至前三个文件描述符中,达到重定向的目的。
2、重定向接口dup2
SYNOPSIS
#include <unistd.h>
int dup2(int oldfd, int newfd);//将oldfd拷贝到newfd中
fd 1
RETURN VALUE
成功返回fd,失败返回-1
如果要输出重定向到fd对应的文件中,那么oldfd就是fd,newfd就是1。
函数功能为将oldfd描述符重定向到newfd描述符,相当于重定向完毕后都是操作oldfd所操作的文件 但是在过程中如果newfd本身已经有对应打开的文件信息,则会先关闭文件后再重定向(否则会资源泄露)
2.1输出重定向
使用dup2对上方输出重定向代码写法进行更改。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd=open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
dup2(fd,1);//输出重定向到fd对应的文件
printf("%d\n",fd);//printf->stdout,在fd对应的文件中打印
fprintf(stdout,"%d\n",fd);//printf->stdout,在fd对应的文件中打印
close(fd);
return 0;
}
在fd对应的文件中进行printf和fprintf打印。
2.2追加重定向
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd=open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
if(fd<0)
{
perror("open");
return 1;
}
dup2(fd,1);//输出重定向到显示器
printf("%d\n",fd);//printf->stdout
fprintf(stdout,"%d\n",fd);//printf->stdout
const char* str="jly";
write(1,str,strlen(str));//将更多内容写入1中
close(fd);
return 0;
}
在fd对应的文件中进行printf和fprintf打印并进行write追加写入。
2.3输入重定
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd=open("log.txt",O_RDONLY);
if(fd<0)
{
perror("open");
return 1;
}
dup2(fd,0);//输入重定向,将fd的内容拷贝至0
char line[60];
while(1)
{
if(fgets(line,sizeof(line),stdin)==NULL)//因为上方输入重定向了,所以这里stdin中会获取log.txt的内容
{
break;
}
printf("%s",line);
}
close(fd);
return 0;
}
如果一个进程打开了文件并创建了子进程,子进程将会拷贝父进程的PCB及文件描述符表等大部分信息,包括曾经父进程打开的文件信息,但是文件是不会被拷贝的;只要一个进程退出,它的PCB、文件描述符表要被释放,指向文件的指针就变少。
六、Linux中一切皆文件
打开的struct file中会有一个函数指针,会作对应的初始化,指向对应驱动层的读写方法,通过读写方法,完成对应设备的读写。
用户站在struct file(文件的结构体对象)上层看来,所有的设备和文件,统一都是struct file,至于struct file内部是如何使用函数指针来调用对应设备文件的读写方法,用户并不了解也并不关心。所以在用户的视角下,Linux一切皆文件。