1.C语言操作文件接口(回顾)
fopen,fwrite,fread,fseek,fclose这些函数都是库函数,是c库当中提供能给程序员调用的函数。
1.1 FILE *fopen(const char *path,const char *mode)
函数描述:
path:待打开的文件(文件路径+文件名称)
mode:以何种方式打开
- r —— 以只读方式打开,当文件不存在的时候,就会打开失败
- r+ —— 以读写方式打开,当文件不存在的时候,就会打开失败
- w —— 以只写方式打开,如果文件不存在,则创建文件。如果文件存在,则会截断(清空)文件
- w+ —— 以读写方式打开,如果文件不存在,则创建文件。如果文件存在,则会截断(清空)文件
- a —— 以追加方式打开,只支持写,如果文件不存在,则创建文件。当前的文件流指针指向了文件的末尾
- a+ —— 以追加方式打开,支持读和写。如果文件不存在,则创建文件。当前的文件流指针指向了文件的末尾
返回值:打开成功返回文件流指针,打开失败返回NULL。
示例:
- 示例1:以 r,r+ 方式打开
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//以只读方式打开文件linux
FILE *fp = fopen("./linux","r"); // 不管是r还是r+,如果文件不存在都会打开失败
if(!fp)
{
perror("fopen");
return -1;
}
printf("open success\n");
return 0;
}
//输出结果:
fopen: No such file or directory
创建linux文件,再次以只读方式打开,结果如下:
[test@localhost c_file]$ touch linux
[test@localhost c_file]$ ./test_file
open success
- 示例2:以 w,w+方式打开
首先删掉前面创建的linux文件,然后以只写方式打开:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
FILE *fp = fopen("./linux","w"); //不管是w还是w+,如果文件不存在都会创建文件
//如果存在,则清空文件
if(!fp)
{
perror("fopen");
return -1;
}
printf("open success\n");
return 0;
}
//输出结果:
open success
此时查看文件:
[test@localhost c_file]$ make
gcc test.c -o test_file
[test@localhost c_file]$ ./test_file
open success
[test@localhost c_file]$ ls
linux makefile test.c test_file
现在linux文件存在,往这个文件中随便写入数据,然后再次以只写方式打开文件:
[test@localhost c_file]$ cat linux
Hello World
[test@localhost c_file]$ ./test_file
open success
[test@localhost c_file]$ cat linux
[test@localhost c_file]$ #文件内容已经被清空
- 示例3:以a,a+方式打开
首先删掉前面创建的linux文件,然后以追加方式打开:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
FILE *fp = fopen("./linux","a"); //不管是a还是a+,如果文件不存在都会创建文件
//如果存在,当前文件流指针指向文件末尾,并不会清空文件内容
if(!fp)
{
perror("fopen");
return -1;
}
printf("open success\n");
return 0;
}
//输出结果:
open success
现在linux文件存在,往这个文件中随便写入数据,然后再次以追加方式打开文件:
[test@localhost c_file]$ echo "Hello world" >> linux
[test@localhost c_file]$ cat linux
Hello world
[test@localhost c_file]$ ./test_file
open success
[test@localhost c_file]$ cat linux
Hello world #可以看到文件内容没有被清空
1.2 size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream)
函数描述:
ptr:要往文件当中写的内容
size:写入块的大小,单位是字节
nmemb:块的个数,单位是个(写入文件字节的数量:size * nmemb)
注意:一般在程序中使用时,是将size设置为1,则nmemb就表示写入的字节数量。
stream:文件流指针
返回值:返回写入成功块的个数,切记不是写入成功字节的数量。
示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE *fp = fopen("./linux","w+"); //以读写方式打开文件
if(!fp)
{
perror("fopen");
return -1;
}
printf("open success\n");
//创建buf数组用于保存需要往文件写入的数据
char buf[1024] = {0};
const char *ptr = "Hello World!";
strncpy(buf,ptr,strlen(ptr));
size_t ret = fwrite(buf,1,strlen(ptr),fp);
printf("ret:%d\n",ret); // 返回的是写入成功块的个数
return 0;
}
输出结果:
[test@localhost c_file]$ ./test_file
open success
ret:12
[test@localhost c_file]$ cat linux
Hello World![test@localhost c_file]$
1.3 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
函数描述:
ptr:要将读到的内容保存在哪里
size:每次读块的大小
nmemb:块的个数
stream:文件流指针
返回值:成功读到的块的个数,返回0说明读取成功了但是没有读到内容。
示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE *fp = fopen("./linux","r");
if(!fp)
{
perror("fopen");
return -1;
}
printf("open success\n");
// 创建数组用以保存读取的数据
char buf[1024] = {0};
size_t ret = fread(buf,1,sizeof(buf)-1,fp);
printf("buf:%s\n",buf);
printf("ret:%d\n",ret); // 返回的是读到的块的个数
return 0;
}
输出结果:
[test@localhost c_file]$ ./test_file
open success
buf:Hello world
ret:12
1.4 int fseek(FILE *stream, long offset, int whence)
函数描述:
stream:文件流指针
offset:偏移量
whence:
- SEEK_SET:文件流指针偏移到文件头部
- SEEK_CUR:文件流指针偏移到当前位置
- SEEK_END:文件流指针指向文件末尾位置
示例:
在一个程序中在文件写入数据后,想要继续读取数据,此时就用到了fseek函数
不使用fseek:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE *fp = fopen("./linux","w+");
if(!fp)
{
perror("fopen");
return -1;
}
printf("open success\n");
char buf_w[1024] = {0};
const char *ptr = "Hello World!";
strncpy(buf_w,ptr,strlen(ptr));
size_t ret_w = fwrite(buf_w,1,strlen(ptr),fp);
printf("ret_w:%d\n",ret_w);
//fseek(fp,0,SEEK_SET); //将文件流指针偏亮到文件头,偏移量为0
char buf_r[1024] = {0};
size_t ret_r = fread(buf_r,1,sizeof(buf_r)-1,fp);
printf("buf_r:%s\n",buf_r);
printf("ret_r:%d\n",ret_r);
return 0;
}
输出结果:
[test@localhost c_file]$ ./test_file
open success
ret_w:12
buf_r: #此时文件流指针指在文件末尾,所以没有读取到
ret_r:0 # 返回值为-1,表示fread函数调用错误,为0,则表示读取成功了,但是没有读到内容
使用fseek后:
[test@localhost c_file]$ ./test_file
open success
ret_w:12
buf_r:Hello World!
ret_r:12
[test@localhost c_file]$ cat linux
Hello World![test@localhost c_file]$
1.5 int fclose(FILE *stream)
关闭文件流指针
fclose(fp); // fp 文件流指针
2. 系统文件IO
—— 系统调用函数的操作文件接口
open,write,read,lseek,close这些函数都是系统调用,是操作系统内核为程序员提供的函数
2.1 int open(const char *pathname, int flags, mode_t mode)
函数描述:
pathname:要打开的文件名称(路径+名称)
flags:以何种方式打开
-
必须的宏,三个宏有且只能出现一个
- O_RDONLY —— 只读方式
- O_WRONLY —— 只写方式
- O_RDWR —— 读写方式
-
可选的宏
- O_APPEND —— 追加
- O_TRUNC —— 截断
- O_CREAT —— 文件不存在则创建
使用方式:必须的宏和可选的宏之间使用按位或的方式(部分)
- O_RDONLY —— 八进制的0
- O_WRONLY —— 八进制的1
- O_RDWR —— 八进制的2
- O_CREAT —— 八进制的100
例:O_RDWR | O_CREAT (是按照位图的方式来使用的)
mode:权限,给新创建出来的文件设置权限,传参的时候,传八进制数字就可以了。
返回值:打开成功,返回大于等于0的数字,是文件描述符,打开失败,返回-1。
示例:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd = open("./linux",O_RDWR | O_CREAT,0664);
if(fd<0)
{
perror("open");
return -1;
}
printf("open success\n");
printf("fd:%d\n",fd);
return 0;
}
输出结果:
[test@localhost sys_file]$ ./sys_file
open success
fd:3
[test@localhost sys_file]$ ls
linux makefile sys_file test.c
2.2 ssize_t write(int fd, const void *buf, size_t count)
函数描述:
fd:文件描述符,open的返回值
buf:往文件里写的内容
count:写的内容的大小
返回值:写成功的字节数量
示例:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd = open("./linux",O_RDWR | O_CREAT,0664);//以读写方式打开文件,如果文件不存在,则创建
if(fd<0)
{
perror("open");
return -1;
}
printf("open success\n");
printf("fd:%d\n",fd);
char buf[1024] = {0};
const char *ptr = "Hello World!";
strncpy(buf,ptr,strlen(ptr));
write(fd,buf,strlen(ptr));
return 0;
}
输出结果:
[test@localhost sys_file]$ ./sys_file
open success
fd:3
[test@localhost sys_file]$ cat linux
Hello World![test@localhost sys_file]$
2.3 ssize_t read(int fd, void *buf, size_t count)
函数描述:
fd:文件描述符,open的返回值
buf:要将读到的内容放到哪里去
count:最大可以读多少个单位字节
返回值:返回读到的字节数量,返回0说明读取成功了,但是没有读取到内容
示例:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd = open("./linux",O_RDWR | O_CREAT,0664);
if(fd<0)
{
perror("open");
return -1;
}
printf("open success\n");
printf("fd:%d\n",fd);
char buf[1024] = {0};
read(fd,buf,sizeof(buf)-1); //-1 是为了给 '\0'留一个位置
printf("buf:%s\n",buf);
return 0;
}
输出结果:
[test@localhost sys_file]$ ./sys_file
open success
fd:3
buf:Hello World!
2.4 off_t lseek(int fd, off_t offset, int whence)
函数描述:
fd:文件描述符
offset:偏移量
whence:
- SEEK_SET:文件流指针偏移到文件头部
- SEEK_CUR:文件流指针偏移到当前位置
- SEEK_END:文件流指针指向文件末尾位置
示例:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main()
{
//打开文件
int fd = open("./linux",O_RDWR | O_CREAT,0664);
if(fd<0)
{
perror("open");
return -1;
}
printf("open success\n");
printf("fd:%d\n",fd);
//写
char buf_w[1024] = {0};
const char *ptr = "Hello World!";
strncpy(buf_w,ptr,strlen(ptr));
write(fd,buf_w,strlen(ptr));
//lseek(fd,0,SEEK_SET); //将文件流指针偏移到文件头
//读
char buf_r[1024] = {0};
read(fd,buf_r,sizeof(buf_r)-1); //-1 是为了给 '\0'留一个位置
printf("buf_r:%s\n",buf_r);
return 0;
}
不使用lseek输出结果:
[test@localhost sys_file]$ ./sys_file
open success
fd:3
buf_r: #此时文件流指针指在文件末尾,所以没有读取到
使用lseek之后:
[test@localhost sys_file]$ ./sys_file
open success
fd:3
buf_r:Hello World!
2.5 int close(int fd)
关闭文件描述符
close(fd); //fd 文件描述符
close(0); // 关闭标准输入
close(1); // 关闭标准输出
close(2); // 关闭标准错误
3. 文件描述符
通过对open函数的学习与理解,文件描述符fd就是一个整数。
0&1&2
操作系统会为每一个进程在磁盘上创建一个以进程号命名的文件夹,在该文件夹下有一个fd文件夹,保存的信息即为该进程打开的文件描述符信息。
下面来一段代码演示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
int fd = open("./linux",O_RDWR | O_CREAT,0644);//打开文件
if(fd<0)
{
perror("open");
return -1;
}
printf("fd:%d\n",fd);
while(1)
{
sleep(1);
}
return 0;
}
//输出结果
fd:3 //然后程序死循环,方便查看信息
查看该进程的文件描述符信息:
可以看到,当我们新创建出来一个进程,势必会打开3个文件描述符,分别对应,标准输入 (0),标准输出 (1),标准错误 (2)。
如图所示,当 ./main运行该程序时:
可以看到,文件描述符其实就是在内核当中的fd_array数组的下标。
文件描述符的分配规则
通过上面的代码发现,打开新的文件后,fd的值为3,那么我们关闭0或者2再看
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("./linux",O_RDWR | O_CREAT,0644);//打开文件
if(fd<0)
{
perror("open");
return -1;
}
printf("fd:%d\n",fd);
while(1)
{
sleep(1);
}
return 0;
}
//输出结果:
fd:0 //然后程序死循环,方便查看信息
如图所示:关闭0以后
如图所示:关闭2以后
结论:文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符即最小未占用原则。
文件描述符与文件流指针的区别
如图所示:
区别:
- 文件流指针是fopen函数返回的,文件流指针是属于c库在维护的
- 文件描述符是open函数返回的,文件描述符是内核在维护的。
- 不同的文件流指针,在c库当中会创建不同的struct _IO_FILE,在_IO_FILE 结构体当中保存了不同的文件描述符。
- 文件流指针当中包含文件描述符
- 操作系统广泛存在缓冲区,但是如果针对文件流指针而言的缓冲区,是c库在维护的。
- exit函数在退出进程时,会刷新缓冲区,就是因为操作的是文件流指针
- _exit函数在退出进程时,不会刷新该缓冲区,是因为该缓冲区是c库在维护的,内核并不知道,所以不会刷新。
文件描述符(文件句柄)泄露的问题
当我们打开一个文件,操作系统会给程序分配一个文件描述符,如果在使用完毕之后,没有及时关闭文件,就会造成文件句柄泄露。
一个进程打开文件的最大数量
-
命令行查看:
可以使用ulimit -[选项][值] 修改对应内容,比如打开文件最大数量。 -
代码查看:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
int fd_count=0;
while(1)
{
int fd = open("./linux",O_RDWR | O_CREAT,0664); // 一直打开文件
if(fd<0)
{
perror("open");
break;
}
printf("fd:%d\n",fd);
fd_count++;
}
printf("fd_count:%d\n",fd_count);
return 0;
}
输出结果:
[test@localhost fd]$ ./fd_count
fd:3
fd:4
fd:5
fd:6
fd:7
……
fd:1019
fd:1020
fd:1021
fd:1022
fd:1023
open: Too many open files #到1024就停下来了
fd_count:1021 #从文件描述符3开始到1023
[test@localhost fd]$
由结果可得知:fd从3开始是因为新创建出来一个进程,操作系统势必会打开3个文件描述符,即 0(标准输入)、1(标准输出)、2(标准错误)
4. 重定向
命令行感受
- 清空重定向
[test@localhost dup]$ echo "Hello" > linux #将Hello重定向到linux文件中
[test@localhost dup]$ cat linux
Hello
[test@localhost dup]$ echo "linux" > linux #清空linux文件内容,再把linux重定向到linux文件中
[test@localhost dup]$ cat linux
linux
[test@localhost dup]$
- 追加重定向
[test@localhost dup]$ echo "Hello" >> linux
[test@localhost dup]$ cat linux
linux
Hello
[test@localhost dup]$ #并没有清空文件内容而是追加在后面
重定向的本质如图所示:
重定向接口 int dup2(int oldfd,int newfd)
流程:
- 关闭1号文件描述符
1.1 关闭成功(返回文件描述符),才能进行第二步
1.2 关闭失败(返回-1),不能进行第二步,重定向失败 - newfd,拷贝oldfd所描述的文件信息
代码演示:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
//打开文件,不存在则创建
int fd = open("./linux",O_CREAT | O_RDWR,0664);
if(fd<0)
{
perror("open");
}
printf("fd:%d\n",fd);
//将标准输出重定向到文件当中
//dup2(int oldfd,int newfd)
//oldfd --> fd(上面打开文件的文件描述符) newfd --> 1(标准输出)
// dup2(fd,1);
int ret_d = dup2(fd,1);
printf("ret_d:%d\n",ret_d);//返回的是1这个文件描述符
//成功以后,下面的代码在往标准输出当中进行输出的时候,就是往文件当中写了
printf("Hello World\n");
// close(fd);
while(1)
{
sleep(1);
}
return 0;
}
//输出结果:
fd:3 //之后陷入死循环,为了方便查看文件描述信息
可以发现已经将标准输出重定向到了文件当中,验证一下是否打印到了文件当中
[test@localhost dup]$ cat linux
ret_d:1
Hello World #可以看到dup2函数之后的两条打印语句都打印到了linux这个文件中