10.文件IO
10.文件IO
从本章开始学习各种Linux系统函数,这些函数的用法必须结合Linux内核的工作原理来理解,因为系统函数正是内核提供给应用程序的接口,而要理解内核的工作原理,必须熟练掌握C语言,因为内核也是用C语言写的,我们在描述内核工作原理时必然要用“指针”、“结构体”、“链表”这些名词来组织语言,就像只有掌握了英语才能看懂英文书一样,只有学好了C语言才能看懂我描述的内核工作原理。
1.C库IO函数的工作流程
C语言操作文件相关问题:
使用fopen函数打开一个文件,返回一个FILE* fp,这个指针指向的结构体有三个重要的成员。
▶ 文件描述符: 通过文件描述可以找到文件的inode,通过inode可以找到对应的数据块
▶ 文件指针: 读和写共享一个文件指针,读或者写都会引起文件指针的变化
▶ 文件缓冲区: 读或者写会先通过文件缓冲区,主要目的是为了减少对磁盘的读写次数,提高读写磁盘的效率。
备注:
▶ 头文件stdio.h 的第48行处: typedef struct _IO_FILE FILE;
▶ 头文件libio.h 的第241行处: struct _IO_FILE,这个接头体定义中有一个_fileno成员,这个就是文件描述符
2. C库函数与系统函数的关系
系统调用: 由操作系统实现并提供给外部应用程序的编程接口,(Application Programming Interface, API),是应用程序同系统之间数据交互的桥梁。
库函数与系统函数的关系:
库函数和系统函数的关系是: 调用和被调用的关系;库函数是对系统函数的进一步封装。
3.虚拟地址空间
进程的虚拟地址空间分为用户区和内核区,其中内核区是受保护的,用户是不能够对其进行读写操作的;内核区中很重要的一个就是进程管理,进程管理中有一个区域就是PCB(本质是一个结构体);
PCB中有文件描述符表,文件描述符表中存放着打开的文件描述符,涉及到文件的IO操作都会用到这个文件描述符。
man 2 write系统调用
man 3
标准C语言函数
命令行参数:
1.Linux每一个运行的程序(进程)操作系统都会为其分配一个0-4G的地址空间(虚拟地址空间)以满足进程序需要资源(CPU资源、内存资源、终端设备),一个程序运行起来一般需要三个终端:标准输入、标准输出、标准错误输出。
2.以32位操作系统为例介绍具体流程
①32位操作系统最大寻址范围是0-4G(2的32次方),是虚拟地址空间
②0-3G是用户区,3G-4G是内核区
用户区:
a.环境变量:环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。
b.命令行参数:main函数最常用的有两个argc和argv
给main()函数传参
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day3$ ls
fun1.c fun2.c head.h main main.o sum.c
fun1.o fun2.o linux系统编程第3天 main.c makefile sum.o
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day3$ ./ main hello ni hao
bash: ./: 是一个目录
cmt@cmt-VMware-Virtual-Platform:~/C_Program/day3$ ./main hello ni hao
this is main!
[0]:[./main]
[1]:[hello]
[2]:[ni]
[3]:[hao]
this is fun1
this is fun2
sum(10)==[45]
.bss段存放未初始化全局变量、.data段存放已初始化的全局变量,text是代码段
内核区:不能读写,不能直接操作内核。
进程管理:操作系统核心通过PCB管理进程,一般PCB是常驻内存的,尤其是调度信息必须常驻内存 。
4.pcb和文件描述符表
备注:
pcb:结构体:task_stuct,该结构体在:
/usr/src/linux-headers-4.4.0-97/include/linux/sched.h:1390
一个进程有一个文件描述符表:1024
-
前三个被占用,分别是
STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO
-
文件描述符作用:通过文件描述符找到inode,通过inode找到磁盘数据块。
虚拟地址空间->内核区->PCB->文件描述表->文件描述符->文件IO操作使用文件描述符
进程管理模块有一个PCB,PCB本质上是一个结构体,进程控制块里有一个文件描述附表,表里面存放的是打开的文件描述符,文件描述符标识了这个文件。
至少打开了三个文件:标准输入、标准输出、标准错误输出。
新打开一个文件时为其分配一个文件描述符,从最小且未被分配的文件描述符开始。如果0这个文件描述符被关闭了,现在新打开一个文件描述符分配的就是0。一个程序最多打开1024个文件描述符。
FILE *fp = fopen();
int fd =open();
fp指向的空间包括fd、filepointer、file buffer
作业:
将昨天的add.c sub.c mul.c divd.c编写makefile文件生成库文件;-----makefile的名字为mathmak,并编写main.c主程序调用库文件的makefile,名字为mainmak。
5.文件IO
从本章开始学习各种Linux系统函数,这些函数的用法必须结合Linux内核的工作原理来理解,因为系统函数正是内核提供给应用程序的接口,而要理解内核的工作原理,必须熟练掌握C语言,因为内核也是用C语言写的,我们在描述内核工作原理时必然要用“指针”、“结构体”、“链表”这些名词来组织语言,就像只有掌握了英语才能看懂英文书一样,只有学好了C语言才能看懂我描述的内核工作原理。
5.1C标准函数与系统函数的区别
什么是系统调用
由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。
一个helloworld如何打印到屏幕。
每一个FILE文件流(标准C库函数)都有一个缓冲区buffer,默认大小8192Byte。Linux系统的IO函数默认是没有缓冲区。
5.2open/close
文件描述符
一个进程启动之后,默认打开三个文件描述符:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
新打开文件返回文件描述符表中未使用的最小文件描述符,调用open函数可以打开或创建一个文件,得到一个文件描述符
open函数
█函数描述: 打开或者新建一个文件
█函数原型:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
█函数参数:
▶pathname
参数是要打开或创建的文件名,和fopen
一样,pathname
既可以是相对路径也可以是绝对路径。
▶flags
参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or。
☆必选项:以下三个常数中必须指定一个,且仅允许指定一个。
●O_RDONLY
只读打开
●O_WRONLY
只写打开
●O_RDWR
可读可写打开
☆以下可选项可以同时指定0个或多个,和必选项按位或起来作为flags参数。可选项有很多,这里只介绍几个常用选项:
●O_APPEND
表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
●O_CREAT
若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该文件的访问权限。
☀文件最终权限:mode & ~umask
●O_EXCL
如果同时指定了O_CREAT,并且文件已存在,则出错返回。
●O_TRUNC
如果文件已存在,将其长度截断为为0字节。
●O_NONBLOCK
对于设备文件,以O_NONBLOCK
方式打开可以做非阻塞I/O(NonblockI/O)
,非阻塞I/O。普通文件没有阻塞非阻塞。
█函数返回值:
▶成功: 返回一个最小且未被占用的文件描述符
▶失败: 返回-1,并设置errno值。
5.3close函数
█函数描述: 关闭文件
█函数原型: int close(int fd);
█函数参数: fd文件描述符
█函数返回值:
▶成功返回0
▶失败返回-1,并设置errno值。
需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
5.4read/write
read函数
█函数描述: 从打开的设备或文件中读取数据
█函数原型: ssize_t read(int fd, void *buf, size_t count);
█函数参数:
▶fd: 文件描述符
▶buf: 读上来的数据保存在缓冲区buf中
▶count: buf缓冲区存放的最大字节数
█函数返回值:
▶ >0:读取到的字节数
▶ =0:文件读取完毕
▶ -1: 出错,并设置errno
write
█函数描述: 向打开的设备或文件中写数据
█函数原型: ssize_t write(int fd, const void *buf, size_t count);
█函数参数:
▶fd:文件描述符
▶buf:缓冲区,要写入文件或设备的数据
▶count:buf中数据的长度
█函数返回值:
▶成功:返回写入的字节数
▶错误:返回-1并设置errno
5.5lseek
所有打开的文件都有一个当前文件偏移量(current file offset
),以下简称为cfo。cfo通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。读写操作通常开始于 cfo,并且使 cfo 增大,增量为读写的字节数。文件被打开时,cfo 会被初始化为 0,除非使用了 O_APPEND
。
使用 lseek 函数可以改变文件的 cfo。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
█函数描述: 移动文件指针
█函数原型: off_t lseek(int fd, off_t offset, int whence);
█函数参数:
▶ fd:文件描述符
▶ 参数 offset
的含义取决于参数 whence:
◆如果 whence
是 SEEK_SET
,文件偏移量将设置为 offset。
◆如果 whence
是 SEEK_CUR
,文件偏移量将被设置为 cfo 加上 offset,offset 可以为正也可以为负。
◆如果 whence
是 SEEK_END
,文件偏移量将被设置为文件长度加上 offset,offset 可以为正也可以为负。
◆函数返回值: 若lseek
成功执行,则返回新的偏移量。
◆lseek函数常用操作
▶文件指针移动到头部
lseek(fd, 0, SEEK_SET);
▶ 获取文件指针当前位置
int len = lseek(fd, 0, SEEK_CUR);
▶获取文件长度
int len = lseek(fd, 0, SEEK_END);
▶ lseek实现文件拓展
off_t currpos;// 从文件尾部开始向后拓展1000个字节
currpos = lseek(fd, 1000, SEEK_END); // 额外执行一次写操作,否则文件无法完成拓展
write(fd, “a”, 1); // 数据随便写
练习:
1 编写简单的IO函数读写文件的代码
2 使用lseek函数获取文件大小
3 使用lseek函数实现文件拓展
例子:
//IO函数测试--->open close read write lseek
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//打开文件
int fd = open("./testlog", O_RDWR | O_CREAT, 0777);//0表示八进制
if(fd < 0)
{
perror("open error");
return -1;
}
//写文件
//ssize_t write(int fd, const void buf[.count], size_t count);
write(fd, "hello world", strlen("hello world"));
//读文件
//ssize_t read(int fd, void buf[.count], size_t count);
char buf[1024];
memset(buf, 0x00, sizeof(buf));
int n = read(fd, buf, sizeof(buf));
printf("n == [%d], buf == [%s]\n", n, buf);
//关闭文件
close(fd);
return 0;
}
buf显示空白没有读到内容,但是文件有内容,是因为文件指针移动到最后,从指针位置开始读,读不到。
解决方法,将文件指针移动到文件开始处。
man搜索的话,不加2搜的是命令,没有函数原型,2搜的是系统函数,1搜的是系统命令,3搜的是系统库函数。read既有函数也有命令。
把文件命令不写死,用命令行参数。
//IO函数测试--->open close read write lseek
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//打开文件
int fd = open(argv[1], O_RDWR | O_CREAT, 0777);//0表示八进制
if(fd < 0)
{
perror("open error");
return -1;
}
//写文件
//ssize_t write(int fd, const void buf[.count], size_t count);
write(fd, "hello world", strlen("hello world"));
//移动文件指针到文件开始位置
//off_t lseek(int fd, off_t offset, int whence);
lseek(fd, 0, SEEK_SET);
//读文件
//ssize_t read(int fd, void buf[.count], size_t count);
char buf[1024];
memset(buf, 0x00, sizeof(buf));
int n = read(fd, buf, sizeof(buf));
printf("n == [%d], buf == [%s]\n", n, buf);
//关闭文件
close(fd);
return 0;
}
lseek.c
//lseek函数获取文件大小
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//打开文件
int fd = open(argv[1], O_RDWR);//0表示八进制
if(fd < 0)
{
perror("open error");
return -1;
}
//调用lseek函数获取文件大小
int len = lseek(fd, 0, SEEK_END);
printf("file size:[%d]\n", len);
//关闭文件
close(fd);
return 0;
}
lseek实现文件拓展
//lseek实现文件拓展
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//打开文件
int fd = open(argv[1], O_RDWR);//0表示八进制
if(fd < 0)
{
perror("open error");
return -1;
}
//移动文件到第100个字节处
lseek(fd, 100, SEEK_SET);
//关闭文件
close(fd);
return 0;
}
文件大小没变,需要一次写入操作,才能实现文件拓展
1 hello world^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ @^@^@^@^@H
5.6perror和errno
errno
是一个全局变量,当系统调用后若出错会将errno
进行设置,perror
可以将errno
对应的描述信息打印出来。
如:perror("open");
如果报错的话打印: open:(空格)错误信息
练习:编写简单的例子,测试perror
和errno
。
5.7阻塞和非阻塞:
思考: 阻塞和非阻塞是文件的属性还是read函数的属性?
█普通文件:hello.c
▶默认是非阻塞的
█终端设备:如 /dev/tty
▶默认阻塞
█管道和套接字
▶默认阻塞
练习:
1 测试普通文件是阻塞还是非阻塞的?
2 测试终端设备文件/dev/tty是阻塞还是非阻塞的。
得出结论: 阻塞和非阻塞是文件本身的属性,不是read函数的属性。
unblock_read.c
//验证read函数读普通文件是否阻塞
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//打开文件
int fd = open(argv[1], O_RDWR);//0表示八进制
if(fd < 0)
{
perror("open error");
return -1;
}
//读文件
char buf[1024];
memset(buf, 0x00, sizeof(buf));
int n = read(fd, buf, sizeof(buf));
printf("FIRST:n == [%d], buf == [%s]\n", n, buf);
//再次读文件,验证read函数是否阻塞
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
printf("SECOND:n == [%d], buf == [%s]\n", n, buf);
//关闭文件
close(fd);
return 0;
}
test.log
hello world
思考: 阻塞和非阻塞是文件的属性还是read函数的属性?
通过读普通文件测试得知: read函数在读完文件内容之后,若再次read,则
read函数会立刻返回,表明read函数读普通文件是非阻塞的。
设备文件: /dev/tty 标准输入STDIN_FILENO
通过读/dev/tty终端设备文件,表明read函数读设备文件是阻塞的。
结论: 阻塞和非阻塞不是read函数的属性,而是文件本身的属性。
socket pipe
这两种文件都是阻塞的。
block.c
//验证read函数读普通文件是阻塞的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//打开文件
int fd = open("/dev/tty", O_RDWR);//0表示八进制
if(fd < 0)
{
perror("open error");
return -1;
}
//读文件
char buf[1024];
memset(buf, 0x00, sizeof(buf));
int n = read(fd, buf, sizeof(buf));
printf("FIRST:n == [%d], buf == [%s]\n", n, buf);
//关闭文件
close(fd);
return 0;
}
运行后阻塞等待输入,输入hello world后输出。
block.c
//验证read函数读普通文件是阻塞的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//读标准输入文件
char buf[1024];
memset(buf, 0x00, sizeof(buf));
int n = read(STDIN_FILENO, buf, sizeof(buf));
printf("FIRST:n == [%d], buf == [%s]\n", n, buf);
return 0;
}
运行后阻塞等待输入,输入hello world后输出。标准输入是阻塞的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)