Linux基础——系统IO函数
文章目录
- 本文内容大部分出自对传智播客linux课程内容的总结和课堂笔记。
- 若有常见命令的详细介绍或linux系统的扩展学习的需要,可以点击此处下载PDF书籍(鸟哥私房菜-基础篇、Linux命令速查手册)。
- 涉及到系统函数用法,可查找man文档中的详细介绍linux 中文 man离线手册
- 本文原文可参考我在语雀平台的原版笔记点击此处阅读
1. C库函数
1.1 文件指针FILE*概述
FILE是一个结构体,主要有三部分:文件描述符、 文件读写位置指针、 I/O缓冲区
1.1.1 文件描述符
- 作用:索引到对应的磁盘文件
1.1.2 文件读写位置指针
- 在读写时,应实时注意文件读写指针的位置
1.1.3 I/O缓冲区(一块内存的地址)
-
通过寻址找到对应的内存块
-
为什么需要I/O缓冲区?
- 减小对硬盘的访问次数,提高程序操作文件的效率;内存和硬盘的访问速度不匹配,相差太大,假如没有缓冲区,每次直接从硬盘存取数据,则速度很慢。
-
注意:
- linux系统函数是没有缓存的,要操作它,这块缓存是由我们用户提供的;
- 为了提高程序效率,c库函数在内部封装了一块缓存。
-
将数据从内存的缓冲区刷新到硬盘:
-
刷新缓冲区
- fflush
-
缓冲区已满
-
正常关闭文件
- fclose
- return(main函数)
- exit(main函数)
-
1.2 虚拟地址空间
1.2.1 概述
- 32位的程序在开始执行时,系统会分配给每个进程一个0-4G的地址空间(虚拟地址空间)。需要注意的是,开始执行一个程序时内存并没有少4个G,而是有4个G的空间供程序操作,实际用了多少空间,内存才少多少空间。
- 其中地址0-3G为用户区,地址3G-4G为内核区,内核区是受保护的,用户不能对该空间进行读写,否则会出现错误。
1.2.2 内核区
- 内核区通常执行内存管理、进程管理、设备驱动管理、VFS虚拟文件系统等任务。
1.2.3 用户区(地址由下至上)
-
受保护的地址(0-4k)
- #define NULL (void*)0 不可访问
-
ELF段(Linux下可执行文件格式为ELF)
- .txt(代码段、二进制机器指令) – 存放代码
- 其他段(只读数据段、符号段等)
- .data(已初始化全局变量)
- .bss(未初始化全局变量)
-
堆空间(向上生长)
-
共享库
-
栈空间(向下生长)
-
命令行参数
-
环境变量(可用env查看每个进程环境变量)
1.2.4 问题:为什么有虚拟地址空间?
CPU为什么要使用虚拟地址空间与物理地址空间映射?解决了什么样的问题?
-
方便编译器和操作系统安排程序的地址分布。
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
-
方便进程之间隔离。
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存。
-
方便OS使用你那可怜的内存。
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,
- 内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
1.3 文件描述符fd
- 位于虚拟地址空间的内核区中有一个PCB(进程控制块),在PCB中有一个文件描述符表,这个表其实是一个整型数组,大小为0-1023。
- 每个位置代表能打开一个文件,每打开一个新文件,则占用一个文件描述符,而且使用的是空闲的最小的一个文件描述符。
- 默认前三个是打开的,分别是标准输入(STDIN_FILENO)、标准输出(STDOUT_FILENO)、标准错误(STDERR_FILENO)。
- 在终端查看文件类型:
file + 文件名
1.4 C库函数与系统函数的关系
-
系统API分为三层:
- 应用层 – 操作的是用户空间
- 系统调用 – 可以操作内核空间
- 内核层 – 设备驱动函数
-
printf函数将一个字符串输出到屏幕的大概流程(如上图):
- 首先printf函数调用应用层的函数write,并将文件描述符fd和字符串传递给它。
- write依然不能直接操作内核区,但是它可以通过系统调用函数sys_write实现从用户空间到内核空间的转换。
- 系统调用sys_write可以操作内核空间,通过调用内核层的设备驱动函数操作硬件层的显示器。
2. 系统I/O函数
2.1 函数open
2.1.1 在man文档中查看一个函数
man + 章节 + 函数名
- e.g.
man 2 open
2.1.2 头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
2.1.3 函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
2.1.4 参数说明
-
pathname: 文件的相对或绝对路径
-
flags: 打开方式
-
位于头文件:fcntl.h
-
必选项(互斥–只能选一个)
- O_RDONLY 只读打开
- O_WRONLY 只写打开
- O_RDWR 可读可写打开
-
可选项
-
O_APPEND 表示追加。
- 如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
-
O_CREAT 若此文件不存在则创建它。
-
使用此选项时需要提供第三个参数mode,表示该文件的访问权限。
-
文件权限由open的mode参数和当前进程的umask掩码共同决定。
-
文件权限 = mode(8进制数) & ~umask掩码
- 比如:0777 & (~0002) = 0775
-
umask + 掩码
可以指定新的掩码
-
-
O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
-
O_TRUNC 如果文件已存在,则将其长度截断(Truncate)为0字节。O_NONBLOCK 设置文件为非阻塞状态
-
2.1.5 返回值fd
- 成功,返回文件描述符;失败,返回-1。
2.1.6 open使用举例
-
以可读写方式打开已存在文件,打开失败fd为-1。
-
flag = O_RDWR
-
flag = O_RDWR
-
以可读写方式创建新文件。
-
flag = O_RDWR | O_CREAT
-
flag = O_RDWR | O_CREAT
-
打开文件时,判断文件是否已存在。若存在,打开失败,返回-1,不存在,则创建新文件。
-
flag = O_RDWR | O_CREAT | O_EXCL
-
flag = O_RDWR | O_CREAT | O_EXCL
-
将文件截断为0,即打开一个文件时,若文件中有内容,则清除。
-
flag = ORDWR | O_TRUNC
-
flag = ORDWR | O_TRUNC
-
注意: 最好每次对文件操作时,都对函数的返回值进行判断,如果发生错误,perror输出错误信息errno,exit()退出当前进程
2.1.7 常见错误
- 打开文件不存在
- 以写方式打开只读文件(打开文件没有对应权限)
- 以只写方式打开目录
2.2 函数close
2.2.1 头文件
#include <unistd.h>
2.2.2 函数原型
int close(int fd);
2.2.3 参数
- fd文件描述符,指定要关闭的文件
2.2.4 返回值
- 返回0表示成功 , 或者-1表示有错误发生
2.3 全局变量errno
-
头文件:
#include <errno.h>
- 全局变量
- 任何标准C库函数都能对其进行修改(Linux系统函数更可以)
-
错误宏定义位置:
-
第 1 - 34 个错误定义:
- /usr/include/asm-generic/errno-base.h
-
第 35 - 133 个错误定义:
- /usr/include/asm-generic/errno.h
-
是记录系统的最后一次错误代码,代码是一个int型的值
-
每个errno值对应着以字符串表示的错误类型
-
当调用"某些"函数出错时,该函数会重新设置 errno 的值
-
2.4 函数perror
-
头文件:
#include <stdio.h>
-
函数声明:
void perror(const char *s)
-
函数说明:
- 用来将上一个函数发生错误的原因输出到标准设备(stderr)
- 参数 s 所指的字符串会先打印出,后面再加上错误原因字符串
- 此错误原因依照全局变量errno 的值来决定要输出的字符串。
-
例如:
若fd=-1,则输出:
2.5 函数read
2.5.1 头文件
#include <unistd.h>
2.5.2 函数原型
ssize_t read(int fd, void *buf, size_t count);
2.5.3 参数
- fd:文件描述符
- buf:数据缓冲区
- count:请求读取的字节数
2.5.4 描述
- 从文件描述符fd中读取count字节的数据并放入从buf开始的缓冲区中.如果count为零,read()返回0,不执行其他任何操作. 如果count大于SSIZE_MAX,那么结果将不可预料.
2.5.5 返回值
- -1 --> 错误
- >0 --> 读出的字节数
- =0 --> 文件读取完毕
- 当返回值小于指定的字节数时,并不意味着错误,这可能是因为当前可读取的字节数小于指定的字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数据,或者read()被信号中断).
2.6 函数write
2.6.1 头文件
#include <unistd.h>
2.6.2 函数原型
ssize_t write(int fd, const void *buf, size_t count);
2.6.3 参数
- fd:文件描述符
- buf:需要输出的缓冲区
- count:最大输出字节数
2.6.4 描述
- 向文件描述符fd所引用的文件中写入从buf 开始的缓冲区中count字节的数据.POSIX规定,当使用了write()之后再使用read(),那么读取到的应该是更新后的数据. 但请注意并不是所有的文件系统都是POSIX兼容的.
2.6.5 返回值
- -1 --> 失败
- >=0 --> 写入文件的字节数
- 若count为零,对于普通文件无任何影响,但对特殊文件将产生不可预料的后果.
2.7 函数lseek
2.7.1 函数作用
- 修改文件偏移量(读写位置)
2.7.2 头文件
#include <sys/types.h>
#include <unistd.h>
2.7.3 函数原型
off_t lseek(int fd, off_t offset, int whence);
2.7.4 参数
- int fd --> 文件描述符
- off_t offset --> 偏移量
- int whence --> 偏移位置
- SEEK_SET - 从文件头向后偏移
- SEEK_CUR - 从当前位置向后偏移
- SEEK_END - 从文件尾部向后偏移
2.7.5 返回值
- 成功完成后,lseek()返回结果偏移位置,以偏移量为单位,从文件开头开始。
- 发生错误时,将返回值(off_t)-1,并且将errno设置为指示错误。
2.7.6 应用
-
拓展文件空间
- 指定偏移量
- 额外一次写操作
- 应用举例:比如用迅雷下载一个电影,比如是10G,把链接打开后开始下载,刚开始下载时,就会有一个10G的文件,这个文件就称为一个空洞文件,通过文件扩展的得到的,打印不出东西。在使用多线程下载时,操作文件的块是不一样的,每个线程知道写文件中哪个位置,因为文件大小已经有了,就可以在10G文件范围内任意移动文件指针,如果没有这10G文件,前面没有写就无法写后面的。
-
获取文件长度
lseek(fd, 0, SEEK_END);
- 返回值即为文件长度
2.7.7 文件扩展举例
- 只能往后拓展,把一个文件扩展成一个更大的文件,扩展后文件会有空洞。