LinuxC——1.文件读写
LinuxC——1.文件读写
1.❤️文件IO
从CPU到文件是Output的一个过程,从文件到CPU是一个Input的过程,这个过程是以CPU为点的
2.🧡系统函数
- open:打开文件
- close:关闭文件
- read:读数据
- write:写数据
- lseek:移动文件中读写位置
- dup:文件书写位置重定位函数,重定位可以写入另一个文件
- fcntl:文件描述符设置
- ioctl:一个特殊函数
3.💛文件读写的简单例子
- open函数:通过fd,找到块设备文件
- 文件系统是一个程序代码,组织块设备所有文件
- 文件系统属于OS一部分
- 找到文件后,调用块设备驱动,打开文件
- 打开成功,返回非负操作符
- 打开失败,返回-1
- write函数:利用打开成功返回的,向文件里面写数据
- lseek函数:利用文件描述符,将文件读写位置调整到文件相应位置
- why设置文件头
- write的时候,文件读写位置已经到了末尾
- read函数:从文件头开始,读取指定长度的数据到buf中
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = 0;
fd = open("./file.txt", O_RDWR);
if(-1 == fd) {
printf("open fail\n");
return 1;
}else {
printf("open ok\n");
}
char buf[] = "Steve Yu, today is nice";
write(fd, (void *)buf, strlen(buf));
lseek(fd, SEEK_SET, 0);
char buf2[30] = {0};
read(fd, buf2, sizeof(buf));
puts(buf2);
close(fd);
return 0;
}
4.💚文件打开后,OS做了什么?
-
记录打开文件信息
- 程序运行后是一个进程,OS会创建一个task_struct的结构体,记录运行的各种信息
- open将文件打开后,在task_struct会创建一些数据结构,用来创建当前进程的打开文件的信息,后面所有的都依赖这些信息
-
open函数会申请一段内存空间(内核缓存)
- 内核缓存是在OS的一个空间,比如char buf[100],就开辟了缓存
- 由os开辟的叫内核缓存
-
open为啥要开辟内存缓存空间?
内存读写 > 磁盘读写,我们会省时间
-
open仅仅开辟了内存缓存空间,并没有数据,只有读写数据的时候
先将数据读到内核缓存中,之后下层会让内存缓存和磁盘数据进行交换
-
读写的时候,数据的流动?
硬件 -> 驱动缓存 -> 内核缓存 -> 应用程序中的数组
(仅掌握open函数,其余函数已经过时)
5.💙指定宏
- O_RDONLY:只读
- O_WRONLY:只写
- O_RDWR:可读可写
以上三个不可以用 | 进行宏运算,而可以与以下进行宏运算
- O_TRUNC:打开进行清空
- O_APEND:打开后写数据追加
- O_NONBLOCK和O_AYNC:非阻塞,同步
flag参数之O_CREAT、O_EXCL
- O_CREAT:新建一个文件
- O_EXCL:O_EXCL和O_CREAT同时被指定,打开文件,如果文件存在,就报错
可以使用O_RDWR|O_CREAT进行组合,那么可以不存在进行创建,存在不打开的功能
6.💜文件描述符
- 文件描述符是什么?
文件描述符指向打开的文件,后面read/write/close等操作,都是基于文件描述符进行操作
- 文件描述符池
每个程序运行起来后,就是一个进程,系统给每个进程分配01023的描述符的范围,返回的文件描述符,是在01023的某个数字
- 为啥第一个打开的文件,open返回的是3?
open返回的文件描述符是规则的:
-
open返回文件描述符池中,返回当前没用的最小的一个
-
进程运行起来,0/1/2会默认使用,最小没用的是3
- fopen文件指针
fopen是C库标准io函数
fopen成功后,FILE*的文件指针,打开了文件
fopen成功后,返回的是FILE*文件指针,指向打开的文件
- 对于Linux C库,fopen对open进行二次封装
7.❤️errno和perror
Linux中,如果像上述中,打开失败,那么就直接失败,遇到比较难排查的错误原因,难以查处具体错误
- 什么是errno?
errno我们查看man 3 errno中,可以看到,一系列错误宏定义,我们include "errno.h"即可
printf("%d: open error", errno);
这样即可发现17:open error
,打开错误
2.具体错误
我们仅仅知道错误号,不知道具体错误,怎么办呢?
-
perror函数:我们通过man 3 perror,可以查看到错误号的具体使用
perror可以打印的错误号以及具体错误
perror("open fail");
我们可以知道,控制台打印:open fail: File exists
3.使用man 2 open
进行查看ERRORS下错误号
比如EACCES,不允许访问
我们因为记不住错误号,也记不住宏定义,所以,我们有了perror
8.🧡close、write、read
- close函数
close(fd):不主动关闭,应用也会自动关闭fd,但是我们得进行手动关闭
- close函数做了啥
-
open打开的时候会在task struct中创建结构体空间,如果文件关闭,那么该结构体空间被释放
类似free(空间地址)
-
malloc和free是给C调用的库看书,Linux在释放的时候,有自己的释放函数
好习惯必须关闭,否则我们在生产环境,容易内存溢出
9.💛task struct结构体
-
结构体在存在在运行的进程中,在task struct是程序在运行的时候进行开辟
-
进程运行结束,linux系统会调用自己的系统函数,进行释放task struct
10.💚文件描述符
有关0/1/2文件描述符
- stdin,文件描述符为0,0代表键盘,实现键盘输入
键盘->键盘驱动缓存->内核缓存->应用缓存
#include <unistd.h>
#include <stdio.h>
int main() {
int ret = 0;
char buf[30] = {0};
ret = read(0, (void *)buf, 30);
if(-1 == ret) {
perror("read fail");
return -1;
}
puts(buf);
return 0;
}
scanf底层调用read(0, buf, size)这个,这样就能兼容不同操作系统
- close(0),scanf还能工作不?
答案是不可以,用perror打印,会得到bad description的
- stdout,文件描述符为1,使用1,则可以进行显示器显示
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd = 1, ret = 0;
char buf[] = "steve\n";
write(fd, buf, sizeof(buf));
close(fd);
return 0;
}
假设我们write(1, &a, sizeof(a))
, a是一个int值,那么会打印一个(char)a进行
- close(1)
printf不能工作
- stderr, 文件描述符是2,是标准出错输出
使用2文件描述符也能进行打印
1打印普通信息,2打印报错信息
- close(2)
perror是通过2进行打印,如果close(2),则不能进行perror
- 宏定义
0:STDIN_FILENO
1:STDOUT_FILENO
2:STDERR_FILENO
11.💙lseek
lseek用来调整读写的位置,调用成功返回偏移量,调用失败返回-1
我们可以通过lseek获取文件大小
1.fd:打开文件
2.whence:粗定位
SEEK_SET:起始位置
SEEK_CUR:当前读写位置
SEEK_END:文件末尾位置
3.offset:微调
进行偏移,正数向前移动,负数向后
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd = 0, ret = 0;
fd = open("./file.txt", O_RDONLY);
if(-1 == fd) {
perror("file open failed");
exit(-1);
}
ret = lseek(fd, 0, SEEK_END);
printf("文件大小%d\n", ret);
return 0;
}
4.使用od -c filename,可以查看以字符进行显示字符
12.💜进程表和文件描述表
进程表:task_struct
- 这个结构体成员多达300个
- 每个进程运行起来后,linux系统会开辟task_struct 结构体
- task_struct专门用于进程运行中,涉及进程相关信息
13.❤️共享操作文件
- 同一个进程共享相同文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
void print_error(char* str) {
perror(str);
exit(-1);
}
int main() {
int fd1, fd2;
fd1 = open("./file.txt", O_RDWR);
if(-1 == fd1) print_error("open fail");
fd2 = open("./file.txt", O_RDWR);
if(-1 == fd2) print_error("open fail");
// 写入操作
return 0;
}
存在相互覆盖的关系
解决方案:使用O_APPEND进行追加
- 多个进程共享相同文件
gcc share_op_file.c -o a
gcc share_op_file.c -o b
./a
./b
那么会出现覆盖情况
解决方案:也使用O_APPEND进行追加
14.🧡dup和dup2
- int dup(int fd)
-
在unistd.h中
-
dup用来复制文件描述符,得到一个新的文件描述符,指向原来的文件,指向最小没用的那一个
fd2 = dup(fd1)
- int dup2(int oldfd, int newfd)
- 在unistd.h中
- dup2指定一个文件描述符,用来复制文件描述符,得到一个新的文件描述符,如果已经打开,则关闭后再次打开
fd2 = (fd1, 4)
- dup、dup2的意义
实现文件共享操作,不使用APPEND也不会出现文件覆盖。使用dup或dup2的时候,永远只有一个文件表。
15.💛实现文件重定位
步骤:
-
打开文件fd
-
close(1)
-
进行复制fd到1
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int fd1;
fd1 = open("data.txt", O_RDWR|O_CREAT);
close(1);
dup(fd1);
printf("Hello World");
return 0;
}
当我们文件描述符写死了,那么可以进行文件重定位来进行,linux底层的重定位>是使用dup/dup2作为底层的支持
16.fcntl函数
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl是文件控制函数(file control)
-
fd:指向打开文件
-
cmd:控制命令
- F_DUPFD
- 模拟DUP
- F_GETFL、F_SETFL
- 获取设置文件状态标注,比如open没有指定O_APPEND,可以使用fcntl进行补设
- F_GETFD、F_SETFD(文件描述符)
- F_GETOWN、F_SETOWN(OWN)
- F_GETLK、F_SETLK、F_SETLKW(加锁)
先掌握前两个
F_DUPFD
和F_GETFL、F_SETFL
- F_DUPFD
模拟DUP:
fcntl(fd, F_DUPFD, 0);// 第三个参数没有,用0表示
模拟DUP2:
fcntl(fd, F_DUPFD, 1);// 模拟DUP2,进行用1
设置
fcntl(fd, F_SETFL, O_RDWR|O_APPEND); // 修改打开时的Flag,返回新设置的标志
保留原标志,加新标志
flag = fcntl(fd, F_GETFL); // 获取
fcntl(fd, F_SETFL, flag|O_APPEND); // 叠加