【Linux】基础IO(系统文件I/O Open write 文件描述符fd 什么是当前路径? 重新谈论文件 文件操作)
文章目录
-
- 什么是当前路径?
- 重新谈论文件
-
- 文件操作
- 系统文件I/O
-
- Open
- write
- 文件描述符fd
什么是当前路径?
编写代码并运行:
1 #include <stdio.h>2 #include <unistd.h>3 4 int main()5 {6 while(1)7 {8 sleep(1); 9 printf("我是一个进程: %d\\n",getpid());10 }11 return 0;12 }
exe
代表的是当前进程执行的是磁盘的可执行程序。
cwd
叫做当前进程的current working directory
,表示当前进程所在的工作目录。
系统中有一个可以更改目录的方法,chdir
,谁调用chdir
,就更改谁的当前工作目录。
原目录:
[likaixuan1@VM-4-13-centos 20230404-基础IO]$ pwd
/home/likaixuan1/106/20230404-基础IO
[likaixuan1@VM-4-13-centos 20230404-基础IO]$
改改代码:
1 #include <stdio.h>2 #include <unistd.h>3 4 int main()5 {6 chdir("/home/likaixuan1"); 7 while(1) 8 { 9 sleep(1);10 printf("我是一个进程: %d\\n",getpid());11 } 12 return 0; 13 }
编译运行:
输入指令:[likaixuan1@VM-4-13-centos 20230404-基础IO]$ ls /proc/32714 -al
当前进程的cwd变成了/home/likaixuan1
,当前工作目录被修改了。
补充问题:
为什么我们自己写的shell,cd的时候,路径没有变化?
fork创建子进程,子进程有自己的工作目录,执行cd更改的是子进程的目录,子进程执行完毕继续用的是父进程即shell,子进程改了和父进程没有关系,我们看到的是父进程。
重新谈论文件
- 空文件也要在磁盘中占空间。
- 文件 = 内容 + 属性。
- 文件操作 = 对内容 + 对属性 or 对内容和属性。
- 标定一个文件,必须使用:文件路径 + 文件名。(唯一性)
- 如果没有指明对应的文件路径,默认是在当前路径进行文件访问。
- 当我们把fopen、fclose、fread、fwrite等接口写完之后,代码编译之后,形成二进制可执行程序之后,但是没运行,文件对应的操作没有执行。对文件的操作本质是:进程对文件的操作。
- 一个文件没有被打开,不可以直接执行文件访问。(一个文件要被访问,就必须先被打开)被用户进程(调用接口)和操作系统OS(实际的打开文件)打开。还有就是不是所有的磁盘文件都被打开。文件在应用层面可以分两类:1.被打开的文件。2.没有被打开的文件。所以进一步得出结论,文件操作的本质:进程和被打开文件的关系。
文件操作
文件在磁盘,磁盘是硬件,只有操作系统可以管理,那么所有的人想要访问磁盘就不能跨过操作系统,必须得使用操作系统提供的文件级别的系统调用的接口。操作系统只有一个,所以上层语言无论如何变化,a.库函数底层必须调用系统调用接口。b.库函数可以千变万化,但底层不变。
r+:读写(不存在就出错)。
w+:读写(不存在就创建)。
a:追加(append),写在文件末尾。
1 #include <stdio.h>2 #include <unistd.h>3 4 5 #define FILE_NAME "log.txt"6 int main()7 {8 FILE *fp = fopen(FILE_NAME,"w");9 if(NULL == fp)10 {11 perror("fopen");12 return 1;13 }14 15 fclose(fp); 16 17 }
编译运行,我们发现创建了log.txt.文件。
运行前:
运行后:
此时文件大小为0,也就证明了w就是写入,但是文件不存在自动会创建。
现在我们想往里面写入5句话,用fprintf()
接口。
1 #include <stdio.h>2 #include <unistd.h>3 4 5 #define FILE_NAME "log.txt"6 int main()7 {8 FILE *fp = fopen(FILE_NAME,"w");9 if(NULL == fp)10 {11 perror("fopen");12 return 1;13 }14 15 int cnt = 5;16 while(cnt)17 {18 fprintf(fp,"%s:%d\\n","hello 19 }
运行然后cat,我们就看到文件内容写到文件里了。
我们也可以以r方式打开文件,这个叫做读取,
修改代码:
1 #include <stdio.h>2 #include <unistd.h> 3 4 5 #define FILE_NAME "log.txt"6 int main()7 { 8 FILE *fp = fopen(FILE_NAME,"w");9 if(NULL == fp)10 {11 perror("fopen");12 return 1;13 } 14 15 char buffer[64]; 16 while(fgets(buffer,sizeof(buffer)-1,fp) != NULL)17 {18 puts(buffer);//把读的字符串,显示到显示器上。 19 } 20 21 fclose(fp); 22 }
我们使用fgets
,表示以行为单位,从特定的文件当中读取数据,将读到的数据放到s所指向的缓冲区的当中。
运行程序,就可以按照文本行的方式把文件读上去了:
我们以a方式打开,继续修改代码:
1 #include <stdio.h> 2 #include <unistd.h> 5 #define FILE_NAME "log.txt" 6 int main() 7 { 10 FILE *fp = fopen(FILE_NAME,"a");11 if(NULL == fp)12 {13 perror("fopen");14 return 1;15 }23 int cnt = 5;24 while(cnt)25 {26 fprintf(fp,"%s:%d\\n","hello file",cnt--);27 } 28 fclose(fp); 36 }
运行程序:
这就叫做追加。
注意细节:以w方式单纯打开文件,c语言会自动清空内部的数据。
系统文件I/O
Open
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。
我们在系统当中实际上用fopen打开文件底层调用的系统调用接口是open
头文件是:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
我们先介绍:
int open(const char *pathname, int flags, mode_t mode);
我们创建文件时权限是什么由第三个参数mode_t mode
告诉。第一个参数就是当前路径。
返回成功会返回给我们一个文件描述符file descriptor
,失败了-1被设置,errno也会被设置。
第二个参数int flags
,它有很多选项:
O_RDONLY…这些东西叫做宏。
我们一般用C传标记为,一个整数传一个标记位。我们知道一个整数有32个比特位,也就意味着我们可以通过比特位传递选项。
操作系统如何使用比特位传递选项?
一个比特位,一个选项,比特位位置不能重复。
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
#define FOUR 0x8
每一个宏对应的数值,只有一个比特位是1,彼此位置不重叠。
有的开源项目是这样写的:
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
代码:
1 #include <stdio.h>2 #include <unistd.h>3 #define FILE_NAME "log.txt"4 5 //每一个比特位对应的数值,只有一个比特位时1,彼此位置不重叠。6 #define ONE (1<<0)7 #define TWO (1<<1)8 #define THREE (1<<2)9 #define FOUR (1<<3)10 11 void show(int flags)12 {13 if(flags & ONE) printf("one\\n");14 if(flags & TWO) printf("two\\n");15 if(flags & THREE) printf("three\\n");16 if(flags & FOUR) printf("four\\n");17 }18 19 20 int main()21 { 22 show(ONE);23 printf("-------------------------\\n");24 show(TWO);25 printf("-------------------------\\n");26 show(ONE | TWO);27 printf("-------------------------\\n");28 show(ONE | TWO | THREE);29 printf("-------------------------\\n");30 show(ONE | TWO | THREE | FOUR); 31 printf("-------------------------\\n"); 32 }
编译运行:
上面就是标记位传参。
再次看:int open(const char *pathname, int flags, mode_t mode);
第一个参数是对应的路径起始就是我们要打开的文件。第二个是按标记位传参,O_RDONLY
表示只读,O_WRONLY
表示只写,O_RDWR
表示读写。此时这就表示不同的标记位(宏),这些宏是通过不同的比特位来表示不同的含义的。
补充:O_TRUNC
:表示对文件内容做清空。O_APPEND
:表示追加。
下面我们看看int open(const char *pathname, int flags, mode_t mode);
怎么用?
close是关闭文件描述符的。
代码:
#include <stdio.h>#include <unistd.h>#include <sys/stat.h>#include <sys/types.h>#include <fcntl.h>#include <assert.h> #define FILE_NAME "log.txt"int main(){ int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666); if(fd < 0){perror("open");return 1;}close(fd);}
编译运行:
运行前:
运行后:
此时log.txt的权限是664(-rw-rw-r–)和我们用C语言创建的一样。
因为umask默认是0002所以我们的权限最终是664
我们可以设置umask为0,默认权限就是666了。
此时权限就是(-rw-rw-rw-),需要注意的是我们改的是我们自己的文件权限并不影响shell。
write
向文件写入:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
第一个参数fd就是我们想往哪个文件去写,第二个是我们想写的时候对应的缓冲区数据所在地,第三个是缓冲区当中字节个数。返回值是我们所写的字节数。
以前学C的时候我们了解到读写文件两种方案:文本类和二进制类,这里的文件读取分类是语言给我们提供的。传const void *
操作系统看来都是二进制位。
sprintf
是将特定的内容格式化,形成到字符串里面。
代码:
#include <stdio.h>#include <unistd.h>#include <sys/stat.h>#include <sys/types.h>#include <fcntl.h>#include <assert.h>#include <string.h>#define FILE_NAME "log.txt"int main(){ umask(0);int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);if(fd < 0){perror("open");return 1;}int cnt = 5; char outBuffer[64]; while(cnt) { sprintf(outBuffer, "%s:%d\\n","aaaa",cnt--); write(fd,outBuffer,strlen(outBuffer));//向文件中写入string的时候 不用+1 close(fd);}
编译运行就完成了写入:
追加的话:需要使用O_APPEND
。
修改下面代码:
int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);
编译运行看下面实验结果:
想要读取文件:得使用O_RSONLY
read
这个接口表示从一个文件描述符中读取文件,返回ssize_t(有符号整数,可以大于小于等于0)
ssize_t read(int fd, void *buf, size_t count);
从特定的文件(fd)中读到缓冲区里(buf),期望读count个。
代码:
#include <stdio.h>#include <unistd.h> #include <sys/stat.h> #include <sys/types.h>#include <fcntl.h> #include <assert.h>#include <string.h> #define FILE_NAME "log.txt" int main(){ umask(0);int fd = open(FILE_NAME,O_RDONLY); if(fd < 0) { perror("open");return 1; } char buffer[1024];ssize_t num = read(fd,buffer,sizeof(buffer)-1);if(num > 0) { buffer[num] = 0;printf("%s",buffer);} close(fd);}
此时就可以读出文件内容了。
如上,系统调用接口:open / close / write / read
对应C语言是库函数接口:fopen / fclose / fwrite / fread
访问文件时必须使用系统调用接口,C库函数是封装了系统调用接口的。
文件描述符fd
进程可以打开多个文件,系统中一定会存在大量被打开的文件,被打开的文件要被操作系统管理。
如何管理?(先描述,再组织)
操作系统为了管理对应的文件,必定要为文件创建对应的内核数据结构标识文件,这个内核数据结构是struct file{}
,它包含了文件的大部分属性。
三个标准输入输出流:
stdin :键盘
stdout :显示器
stderr :显示器
FILE *fp = fopen();
FILE是一个结构体,我们底层访问文件时必须用系统调用,而系统调用接口访问文件时必须用文件描述符。所以这个结构体里必定有一个字段叫做文件描述符。
输出文件描述符:
我们自己打开的文件或文件描述符是从3开始的,原因就是0 1 2 默认被占用,我们C语言FILE类型指针也封装了操作系统内的文件描述符。
为什么数字是0 1 2开始的?
进程执行了一个接口,接口叫做open,系统在启动的时候默认启动了三个文件:键盘、显示器、显示器。操作系统要把log.txt
加载到对应的内存里,那么首先并不是把内容直接加载到内存里,而是先描述这个文件,那么log.txt
对应的结构叫做struct file
,struct file{}
保存的是文件的属性。PCB里有一个struct files_struct *file
这样的一个指针,这个指针指向了一个属于进程的数据结构对象,叫做struct files_struct
这个结构体是专门构建进程和文件对应关系的结构体,这个结构体里面包含了一个数组,这个数组的名字叫struct file* fd_array[]
这是一个指针数组,数组里面所有的成员都是struct file*
的指针,数组就有下标,分别都可以指向文件,0指向键盘,1指向显示器,2指向显示器,当再打开文件时从上往下找没有被使用的文件标识符了,所以从磁盘中把文件加载进来,把对象构建好,然后把这个对象的地址填到3号文件描述符里,此时3号文件描述符就指向新打开的文件了,然后我们再把3号文件描述符通过系统调用给用户返回,就得到了一个数字就叫做3,所以当一个进程在访问文件时它需要传入3,通过系统调用,操纵系统会找进程的文件描述符表,根据它找到对应的文件,文件找到了就可以对文件进行操作了。
文件描述符的本质:数组的下标。