一、C知识回顾
C语言文件操作的相关接口中,fopen函数的返回值为一个类型为FILE的结构体指针,这个FILE类型即为C语言概念中的描述文件的类型。而C语言库中默认存在着三个输入输出相关的文件流,这三个文件流会在进程运行的时候默认被打开,分别为:
<1> 标准输入:stdin,对应硬件设备:键盘
<2> 标准输出:stdout,对应硬件设备:显示器
<3> 标准错误:stderr,对应硬件设备:显示器
二、文件管理
往显示器上显示,往磁盘文件打开或写入文件,读写键盘,本质上是访问硬件,我们用户在访问硬件,不可能直接通过语言进行直接访问硬件的,必须要通过操作系统,
我们使用的C接口,看起来是直接访问硬件,其实是通过操作系统提供的系统调用接口,才能访问到硬件,所以我们使用的C接口,底层一定要封装对应的文件类的系统调用!
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。
⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝
2.1 open
#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:文件名
- 参数flag:标志位,通过传递不同的标志为可以以不同的访问方式打开文件
- 参数mode:设置文件的初始权限位,可以使用系统接口
umask
来提前设置创建文件的权限掩码- 返回文件描述符fd,数据类型为整形
flag标志位的常用参数
O_RONLY
,只读O_WRONLY
,只写O_RDWR
,读写O_TRUNC
,清空文件,覆盖式写入O_APPEND
,向文件结尾写入,追加O_CREAT
,创建文件
系统所给出的flag参数实质上为一个个定义好的宏,flag参数的类型为int,其有着32个bit位。
<1> 这些宏都是32个bit位中只有一位为1的int类型数据,并且它们的含1bit位都互相错位。
<2> 通过此种位操作宏定义与按位与|的方式,可以达到一次性传递多个标志位参数的效果,Linux操作系统中常用。
#include <stdio.h>
#define ONE (1<<0)//1 000001 左移0位
#define TWO (1<<1)//2 000010 左移1位
#define THREE (1<<2)//4 000100 左移2位
#define FOUR (1<<3)//16 001000
#define FIVE (1<<4)//32 010000
//code 1
void PrintTest(int flags)
{
//都为1才为1,只要有一个不为1,就为0
if(flags & ONE)
{
printf("one\n");
}
if(flags & TWO)
{
printf("two\n");
}
if(flags & THREE)
{
printf("three\n");
}
if(flags & FOUR)
{
printf("four\n");
}
if(flags & FIVE)
{
printf("five\n");
}
}
int main()
{
printf("=====================\n");
PrintTest(ONE);
printf("=====================\n");
PrintTest(TWO);
printf("=====================\n");
PrintTest(THREE);
printf("=====================\n");
//只要两个操作数对应的位中有一个为1,那么结果位就为1。
PrintTest(ONE | THREE);
printf("=====================\n");
PrintTest(ONE | TWO | THREE);
printf("=====================\n");
PrintTest(ONE | TWO | THREE | FOUR);
printf("=====================\n");
return 0;
}
现在我们来使用一下open函数,第二个参数传递 O_WRONLY | O_CREAT,以只写方式打开,如果文件不存在则创建:
#include <stdio.h>
#include <fcntl.h>
int main()
{
//以只写方式打开,如果文件不存在则创建
open("log.txt",O_WRONLY | O_CREAT);
return 0;
}
-rwxrwxr-x 1 zxw zxw 8408 Jan 14 17:52 filecode
-rw-rw-r-- 1 zxw zxw 1193 Jan 14 17:52 filecode.c
-r--r-x--T 1 zxw zxw 0 Jan 14 17:52 log.txt //权限错乱
-rw-rw-r-- 1 zxw zxw 71 Jan 12 17:31 Makefile
这里我们发现新创建的文件,他的权限是错乱的,那是因为我们调用系统接口时,新创建文件,需要给文件进行权限设置,而我们语言级接口fopen不需要,是因为底层对其进行了封装。
所以用系统调用接口时,新建文件,还要告诉新建文件默认的起始权限是多少!告诉第三个接口mode_t mode。
int main()
{
open("log.txt",O_WRONLY | O_CREAT,0666);
return 0;
}
-rwxrwxr-x 1 zxw zxw 8360 Jan 14 18:07 filecode
-rw-rw-r-- 1 zxw zxw 1185 Jan 14 18:07 filecode.c
-rw-rw-r-- 1 zxw zxw 0 Jan 14 18:07 log.txt
-rw-rw-r-- 1 zxw zxw 71 Jan 12 17:31 Makefile
此外为什么我log.txt的权限是-rw-rw-r--而不是-rw-rw-rx-因为我传递的是0666
因为系统里有个umask,他会默认屏蔽掉一些权限,系统的umask = 0002,结合666,就会编成664,所以文件的最终权限会结合umask值来进行最终确认。
int main()
{
//不让系统屏蔽某些权限
umask(0);
//以只写方式打开,如果文件不存在则创建
open("log.txt",O_WRONLY | O_CREAT,0666);
return 0;
}
-rwxrwxr-x 1 zxw zxw 8408 Jan 14 18:17 filecode
-rw-rw-r-- 1 zxw zxw 1198 Jan 14 18:17 filecode.c
-rw-rw-rw- 1 zxw zxw 0 Jan 14 18:18 log.txt
-rw-rw-r-- 1 zxw zxw 71 Jan 12 17:31 Makefile
对在代码中umask清0,并不会影响到系统的umask值(系统的umask还是0002)。最终文件权限就是是第三个参数传递的权限。
2.2 write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- 参数fd:文件描述符,指定要写入的目标对象
- 参数buf:写入数据存储地址,缓冲区
- 参数count:写入数据的大小,单位字节
- 返回值为ssize_t,有符号整形,写入成功返回0,写入失败返回-1
2.3 read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- 参数fd:文件描述符,指定要写入的目标对象
- 参数buf:读取的内容存储空间的地址
- 参数count:读取多少个字节
- 返回值ssize_t,有符号整形,读取成功返回读取了多少个字节,读取失败返回-1
2.4 close
#include <unistd.h>
int close(int fd);
- 传递参数文件fd关闭指定文件
- 关闭成功返回0,关闭失败返回-1
2.5 什么是文件描述符fd
我们重新回到2.1open的返回值那块看看
返回文件描述符fd,数据类型为整形
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
//以只写方式打开,如果文件不存在则创建
int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode
fd1: 3
此时文件标识符为3,为什么文件打开从3开始呢?
因为进程启动,默认打开了三个标准的输入输出流:stdin,stdout,stderr
因为Linux下一切皆文件,这三个标准的输入输出流被当成文件打开了。
现在我们使用系统调用接口来进行文件写入,如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//以只写方式打开,如果文件不存在则创建
int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "hello word\n";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt
hello word
紧接着,我们只修改一个message指向的内容,原本文件内容不变:、
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//以只写方式打开,如果文件不存在则创建
int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "aaaaaa\n";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt
aaaaaa
ord
因为在做操作时只告诉了写入,并没有告诉要清空,只覆盖在原来基础上进行覆盖式的写入!!!
所以我们再加个选项O_TRUNC,如果文件存在则进行先清空
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//以只写方式打开,如果文件不存在则创建,如果存在则清空
int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "aaaaaa\n";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt
aaaaaa
内容被清空了,O_WRONLY | O_CREAT | O_TRUNC传参,最终效果就是和用fopen打开使用“w”方法一样,fopen的“w”方法,底层就是封装了这些。
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode
fd1: 3
[zxw@hcss-ecs-cc58 lesson17]$ cat log.txt
aaaaaa
aaaaaa
O_WRONLY | O_CREAT | O_APPEND传参,最终效果就是和用fopen打开使用“a”方法一样,fopen使用“a”方法底层就是封装了这些。
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。
open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝
fopen fclose fread fwrite底层就是对open close read write进行了封装。
关于fd的问题:
我们连续打开几个文件,看看文件描述符是多少?
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
umask(0);
int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
int fd5 = open("log5.txt", O_WRONLY|O_CREAT|O_TRUNC, 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;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode
fd1 = 3
fd2 = 4
fd3 = 5
fd4 = 6
fd5 = 7
观察到文件描述符从3开始依次创建
前面我们说到,0,1,2被键盘,显示器,显示器占用,又说过,这些硬件在底层被包装成文件的形式,同样,我们能不能通过0,1,2进行对键盘文件,显示器文件进行写与读呢?
看下面代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
const char *message = "hello write\n";
write(1,message,strlen(message));
return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode
hello write
我们通过write,直接向文件描述符1,进行写入message指向的内容,结果的确打印在屏幕上
我们再来看看下面调用read进行读:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//abcd
char buffer[128];
ssize_t s = read(0,buffer,sizeof(buffer));
if(s > 0)
{
//把回车改为0
buffer[s-1] = 0;
printf("%s\n",buffer);
}
return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode
abcd
abcd
我们键盘输入abcd,然后打印,发现的确从标识符0进行读取。
我们来看一下下面代码来进行验证:因为stdin,stdout,stderr是FILE*类型,所以用他进行指向一个成员变量_fileno,就可以看到文件描述符:
int main()
{
printf("stdin:%d\n",stdin->_fileno);
printf("stdout:%d\n",stdout->_fileno);
printf("stderr:%d\n",stderr->_fileno);
FILE *fp = fopen("log.txt","w");
printf("fp:%d\n",fp->_fileno);
return 0;
}
[zxw@hcss-ecs-cc58 lesson17]$ ./filecode
stdin:0
stdout:1
stderr:2
fp:3
所以任你文件怎么样,OS只认文件描述符
在C语言上,用到的函数都是对系统调用的封装
不仅做了接口上的封装,还做了类型上的封装就是struct_file。
2.5.1 重新理解一切皆文件
上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过
struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取Linux系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解。
2.5.2 文本写入VS二进程写入
在计算机里,OS系统层面上只有二进制概念,语言层看起来可以文本写入,也可以二进制写入。
我们向显示器写12345,写的是1‘’2‘’3‘’4‘’5‘字符。
int main()
{
char *message = "hello\n";
write(1,message,strlen(message));
return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test
hello
int main()
{
int a = 12345;
write(1,&a,sizeof(a));
printf("\n");
return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test
90
int main()
{
int a = 12345;
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%d",a);
write(1,buffer,strlen(buffer));
printf("\n");
return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test
12345
所以为什么我们C语言要给我们很多接口做封装?
1.方便用户进行操作
2.提高语言的可移植性
在使用这些接口的时候,win和Linux或者其他平台提前给我们安装了一些东西,这个东西叫glibc的库,语言层使用的一些接口,在glibc中进行了封装,把库编成Linux版本的库,win版本的库等等