LINUX(三)文件I/O、对文件打开、读、写、偏移量
系列文章目录
文章目录
- 系列文章目录
- 概念介绍
- 系统调用
- 文件I/O
- open
- write
- read
- close
- lseek
- 文件解析
- 文件存储与节点区
- 进程操作文件原理
- 返回错误处理与errno
- strerror
- perror函数
- exit、_exit、_Exit
概念介绍
系统调用
system call是linux内核给应用层的API,是进入内核的入口,应用程序用过调用系统接口实现使用内核提供的服务、资源以及各种各样的功能。
驱动开发工程师通过调用 Linux 内核提供的接口完成设备驱动的注册,驱动程序负责底层硬件操作相关逻辑。
Linux 应用编程(系统编程)则指的是基于 Linux 操作系统的应用编程,在应用程序中通过调用系统调用 API 完成应用程序的功能和逻辑,应用程序运行于操作系统之上。
通常在操作系统下有两种不同的状态:内核态和用户态,应用程序运行在用户态、而内核则运行在内核态。
应用编程简单点来说就是:开发 Linux 应用程序,通过调用内核提供的系统调用或使用 C 库函数来开发具有相应功能的应用程序。
文件I/O
文件 I/O(Input、Outout),对文件的读写操作,Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要
文件描述符:file description,非负整数,比如在open函数成功时会返回的这个值,这个值是内核向进程返回的,指代被打开的文件,失败操作会返回-1。
一个进程可以打开多个文件,在 Linux 系统中,一个进程可以打开的文件数是有限制,打开的文件是需要占用内存资源的,如果超过进程可打开的最大文件数限制,内核将会发送警告信号给对应的进程,然后结束进程;可以通过 ulimit 命令来查看进程可打开的最大文件数,一般是1024
ulimit -n
对于一个进程来说,文件描述符是一种有限资源,文件描述符是从 0 开始分配的,进程中第一个被打开的文件对应的文件描述符是 0、第二个文件是 1、第三个文件是 2、第 4 个文件是 3……文件描述符数字最大值为 1023(0~1023)。每一个被打开的文件在同一个进程中都有一个唯一的文件描述符,不会重复,如果文件被关闭后,它对应的文件描述符将会被释放,那么这个文件描述符将可以再次分配给其它打开的文件绑定起来。
一般012这三个文件描述符被系统占用了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2)
硬件设备也对应的文件,叫做设备文件,应用程序通过对设备文件进行读写等操作、来使用、操控硬件设备,譬如 LCD 显示器、串口、音频、键盘等。
标准输入一般是键盘,标准输出一般是LCD屏幕,标准错误一般也是LCD显示器
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);
man命令可以查看帮助信息,譬如函数功能介绍、函数原型、参数、返回值以及使用该函数所需包含的头文件等信息。
2表示系统调用,1代表Linux命令,3表示标准C库函数
man 2 open
open命令有两种,这是可变参数的写法,需要包含三个头文件
参数:
pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信息,譬如:“./src_file”(当前目录下的 src_file 文件)、"/home/dengtao/hello.c"等;如果 pathname 是一个符号链接,会对其进行解引用。
flags:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,都是宏定义常量,可以单独使用某一
个标志,也可以通过位或运算(|)将多个标志进行组合。
还有很多的标志位,O_APPEND、O_ASYNC、O_DSYNC、O_NOATIME、O_NONBLOCK、O_SYNC 以及 O_TRUNC 等,不同内核版本可能也不一样。但这些命令只有文件有相关的权限时,才能正常操作。
mode:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。一般用touch创建文件后,会有默认的权限,经常通过chmod来修改权限
ls -l 查看文件权限
mode参数u32无符号整数,
O—这 3 个 bit 位用于表示其他用户的权限;
G—这 3 个 bit 位用于表示同组用户(group) 的权限,即与文件所有者有相同组 ID 的所有用户;
U—这 3 个 bit 位用于表示 文件所属用户 的权限,即文件或目录的所属者;
S—这 3 个 bit 位用于表示文件的特殊权限,一般不管
然后每3bit都是按照rwx来分配的,read/write/execute,读/写/执行,0表示没有,1表示有
最高权限表示方法:111111111(二进制表示)、777(八进制表示)、511(十进制表示)
111000000(二进制表示):表示文件所属者具有读、写、执行权限,而同组用户和其他用户不具有任何权限;
100100100(二进制表示):表示文件所属者、同组用户以及其他用户都具有读权限,但都没有写、执行权限。
除了自己赋值,还可以用linux里面弄好的宏定义
打开范例:
//可读可写方式打开已经存在的文件
int fd = open("./app.c", O_RDWR)
if (-1 == fd)return fd;//打开指定文件
int fd = open("/home/dengtao/hello", O_RDWR | O_NOFOLLOW);
if (-1 == fd)return fd;
write
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符
buf:指定写入数据对应的缓冲区。
count:指定写入的字节数。
返回值:如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。
read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符。与 write 函数的 fd 参数意义相同。
buf:指定用于存储读取数据的缓冲区。
count:指定需要读取的字节数。
返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节数,也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。
close
关闭文件
#include <unistd.h>
int close(int fd);
fd:文件描述符,需要关闭的文件所对应的文件描述符。
返回值:如果成功返回 0,如果失败则返回-1。
在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件,很多程序都利用了这一功能而不显式地用 close 关闭打开的文件。
文件描述符是有限资源,当不再需要时必须将其释放、归还于系统。
lseek
对于每个打开的文件,系统都会记录它的读写位置偏移量,我们也把这个读写位置偏移量称为读写偏移量,记录了文件当前的读写位置
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用 read()、write()将自动进行累计,指向已读或已写数据后的下一字节
fd:文件描述符。
offset:偏移量,以字节为单位。可以正也可以负。
whence:用于定义参数 offset 偏移量对应的参考值
SEEK_SET:读写偏移量将指向 offset 字节位置处
SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处
SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处
//将读写位置移动到文件开头处
off_t off = lseek(fd, 0, SEEK_SET);
if (-1 == off)return -1;//将读写位置移动到文件末尾:
off_t off = lseek(fd, 0, SEEK_END);
if (-1 == off)return -1;//将读写位置移动到偏移文件开头 100 个字节处
off_t off = lseek(fd, 100, SEEK_SET);
if (-1 == off)return -1;//获取当前读写位置偏移量
off_t off = lseek(fd, 0, SEEK_CUR);
if (-1 == off)return -1;
文件解析
文件存储与节点区
文件是存放在磁盘里面,硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统一般会一次性读取多个扇区,这叫做块,是文件存取的最小单位一个块4KB,所以1个块是8个扇区
磁盘在进行分区、格式化时分为两个区域,数据区,用于存储文件中的数据;== inode 节点区,用于存放 inode table(inode 表)==,每一个文件都必须对应一个 inode,inode 实质上是一个结构体,里面元素记录了文件信息如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)位置等等信息,文件名并不是记录在 inode 中
查看inode编号
ls -istat xxx文件名
之前买的闪迪的U盘,就有数据恢复软件,原因就是其实删除数据时删掉的是inode表,数据内容还在,只有新的数据在存入时才会覆盖掉。
windows里面的快速格式化就是例子,这种格式化会非常快,也是因为删掉的只是inode table表,数据也是可以找回来的。
open打开文件时,内核会申请内存拷贝静态文件,叫动态文件,然后对动态文件进行读写操作,之后再同步更新到设备中。
这就是为啥打开大文件很慢,文档忘记保存丢失数据的原因
硬盘这些块设备,读写起来按块单位操作,内存就能按照字节操作,灵活快速效率高。
进程操作文件原理
在 Linux 系统中,内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写PCB)。
PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表
返回错误处理与errno
errno是一个全局变量,每种错误都对应一个编号,errno存储函数执行错误编号,也意味会覆盖上一次的错误码。
本质是int型变量,并不是执行所有的系统调用或 C 库函数出错时,操作系统都会设置 errno
man 2 xxx
用man打开时,可以看返回值有没有
程序当中包含<errno.h>头文件即可,就能获取errno
#include <stdio.h>
#include <errno.h>
int main(void)
{printf("%d\n", errno);return 0;
}
strerror
调用strerror函数可以将errnoo 转换成适合我们查看的字符串信息,C库函数
#include <string.h>
char *strerror(int errnum);
测试:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void)
{int fd;/* 打开文件 */fd = open("./test_file", O_RDONLY);if (-1 == fd) {printf("Error: %s\n", strerror(errno));return -1;}close(fd);return 0;
}
strerror 返回的字符串是"No such file or directory",可以很直观的知道 open 函数执行的错误原因是文件不存在
perror函数
这个查看错误的函数用的多,不需要传参errno,而且直接打印错误信息,不是返回字符串,还能在打印前添加自己的信息
#include <stdio.h>
void perror(const char *s);
s:在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。
例子:
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {perror("open error");return -1;
}
exit、_exit、_Exit
程序出错的时候要停止,在 Linux 系统下,进程(程序)退出可以分为正常退出和异常退出。
异常往往更多的是一种不可预料的系统异常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等。
进程正常退出除了可以使用 return 之外,还可以使用 exit()、_exit()以及_Exit()