Linux应用开发1 文件I/O 入门看这一份就够了

内容:常用、进阶命令汇总,含解释和例程

注意:此部分不包括标准I/O库的函数介绍,标准I/O库见下一篇

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
 char buff[1024];
 int fd1, fd2;
 int ret;
 /*轴对称开关*/
 /* 打开源文件 src_file(只读方式) */
 fd1 = open("./src_file", O_RDONLY);
 if (-1 == fd1)
 return fd1;
 /* 打开目标文件 dest_file(只写方式) */
 fd2 = open("./dest_file", O_WRONLY);
 if (-1 == fd2) {
 ret = fd2;
 goto out1;
 }
 /* 读取源文件 1KB 数据到 buff 中 */
 ret = read(fd1, buff, sizeof(buff));
 if (-1 == ret)
 goto out2;
 /* 将 buff 中的数据写入目标文件 */
 ret = write(fd2, buff, sizeof(buff));
 if (-1 == ret)
 goto out2;
 ret = 0;
out2:
 /* 关闭目标文件 */
 close(fd2);
out1:
 /* 关闭源文件 */
 close(fd1);
 return ret; }

 文件描述符

        每一个被打开的文件在同一个进程中都有 一个唯一的文件描述符,不会重复,如果文件被关闭后,它对应的文件描述符将会被释放,那么这个文件描述符将可以再次分配给其它打开的文件、与对应的文件绑定起来。
        当我们在程序中,调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始,这里大家可能要问了,上面不是说从 0 开始的吗,确实是如此,但是 0 1 2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2)
Tips:Linux 系统下,一切皆文件,也包括各种硬件设备,使用 open 函数打开任何文件成功情况下便会返回对应的文件描述符 fd。每一个硬件设备都会对应于 Linux 系统下的某一个文件,把这类文件称为设备文件。所以设备文件对应的其实是某一硬件设备,应用程序通过对设备文件进行读写等操作、来使用、操控硬件设备,

在linux下可以通过man手册查看各类linux提供的应用函数的用法

open

open 函数用于打开文件,当然除了打开已经存在的文件之外, 还可以创建一个新的文件
man 2 open #查看 open 函数的帮助信息
在应用程序中使用 open 函数时,需要包含 3 个头文件
#include <sys/types.h>”、“ #include <sys/stat.h> ”、“ #include <fcntl.h> ”。
int open ( const char * pathname , int flags );
        两个参数 第一个是要打开的文件, 第二个是标识类型 
flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合

Tips:man 命令后面跟着两个参数,数字 2 表示系统调用,man 命令除了可以查看系统调用的帮助信息外,还可以查看
Linux 命令(对应数字 1)以及
标准 C 库函数(对应数字 3)所对应的帮助信息;
最后一个参数 open 表示需要查看的系统调用函数名。

write

ssize_t write(int fd, const void *buf, size_t count);
fd 文件描述符。关于文件描述符,前面已经给大家进行了简单地讲解,这里不再重述!我们需要将进
行写操作的文件所对应的文件描述符传递给 write 函数。
buf 指定写入数据对应的缓冲区。
count 指定写入的字节数。

read

ssize_t read(int fd, void *buf, size_t count);
fd 文件描述符。与 write 函数的 fd 参数意义相同。
buf 指定用于存储读取数据的缓冲区。
count 指定需要读取的字节数。

close

int close(int fd);
fd 文件描述符,需要关闭的文件所对应的文件描述符。

lseek(重要,偏移量记录)man 2 lseek

        读写偏移量,记录了文件当前的读写位置,当调用 read()或 write() 函数对文件进行读写操作时,会从当前读写位置偏移量开始进行数据读写

off_t lseek(int fd, off_t offset, int whence);

fd文件描述符。

offset 偏移量,以字节为单位。
whence 用于定义参数 offset 偏移量对应的参考值      
返回值: 成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生错误将返回-1

SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算); 

SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;

SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为 负,如果是正数表示往后偏移,如果是负数则表示往前偏移;

 =========================================================================

 I/O进阶

之前是简单的打开 读 写 关闭 

静态文件

文件存放在磁盘文件系统中,称为静态文件,硬盘的最小存储单位叫做“扇区”,每个扇区储存 512 字节

一次性连续读取多个扇区,即一次 性读取一个“块”,最常见的是 4KB

动态文件

        当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件
        当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设中。

        静态文件以块为单位操作,不方便 动态文件对内存操作,以字节为单位

返回错误处理与 errno

        在 Linux 系统下 对常见的错误做了一个编号 ,每一个编号都代表着每一种不同的错误类型,当函数执行发生错误的时候,操作系统会将这个错误所对应的编号赋值给 errno 变量,每一个进程(程序)都维护了自己的 errno 变量,它是程序中的全局变量,该变量用于存储就近发生的函数执行错误编号,也就意味着下一次的错误码会覆盖上一次的错误码。所以由此可知道,当程序中调用函数发生错误的时候,操作系统内部会通过设置程序的 errno 变量来告知调用者究竟发生了什么错误!
        在我们的程序当中如何去获取系统所维护的这个errno 变量呢?只需要在我们程序当中包含 <errno.h> 头 文件即可,你可以直接认为此变量就是在<errno.h> 头文件中的申明的
#include <stdio.h>
#include <errno.h>
int main(void)
{
printf("%d\n", errno);
return 0;
}
C 库函数 strerror(),该函数可以将对应的 errno 转换成适合我们查看的字符串信息
man 3 strerror
#include <string.h>
char *strerror(int errnum);
errnum 错误编号 errno
返回值: 对应错误编号的字符串描述信息
/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
printf("Error: %s\n", strerror(errno));
return -1;
}

perror 函数 (查看具体的错误类型)(man 3 perror

#include <stdio.h>
void perror(const char *s);
s 在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。
返回值: void 无返回值。
/* 打开文件 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
perror("open error");
return -1;
}

exit_exit_Exit

        当程序在执行某个函数出错的时候,如果此函数执行失败会导致后面的步骤不能在进行下去时,应该在 出错时终止程序运行,不应该让程序继续运行下去,那么如何退出程序、终止程序运行呢?有过编程经验的读者都知道使用 return, 一般原则程序执行正常退出 return 0,而执行函数出错退出 return -1
Linux 系统下,进程正常退出除了可以使用 return 之外,还可以使用 exit() _exit() 以及 _Exit()
_exit() _Exit() 函数
        main 函数中使用 return 后返回, return 执行后把控制权交给调用函数,结束该进程。调用 _exit() 函数会 清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符 ,并结束进程、将控制权交给操作系统。
#include <unistd.h>
void _exit(int status);
eg:_exit(0);

exit()函数 (C库函数)

        exit()函数 _exit() 函数都是用来终止进程的, exit() 是一个标准 C 库函数,而 _exit() _Exit() 是系统调用。 执行exit() 会执行一些清理工作,最后调用 _exit() 函数 

空洞文件(预先申请好地址,就可以从多个地址头中实现多线程操作)

在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势;

O_APPEND O_TRUNC 标志

O_TRUNC 标志
        O_TRUNC 这个标志的作用非常简单,如果使用了这个标志,调用 open 函数打开文件的时候会将文件 原本的内容全部丢弃,文件大小变为 0
O_APPEND 标志
        如果 open 函数携带了 O_APPEND 标志,调用 open 函数打开文件,当每次使用 write() 函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末尾开始写入数据
一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件
一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的

复制文件描述符

        在 Linux 系统中, open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,
复制得到的文件描述符和旧的文件描述符拥有相同的权限

dup 函数 (man 2 dup

#include <unistd.h>
int dup(int oldfd);
oldfd 需要被复制的文件描述符。
返回值: 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则; 如果复制失败将返回-1 ,并且会设置 errno 值。
 /* 复制文件描述符 */
 fd2 = dup(fd1);
 if (-1 == fd2) {
 perror("dup error");
 ret = -1;
 goto err1;
 }
 printf("fd1: %d\nfd2: %d\n", fd1, fd2);
dup2 函数 dup的文件描述符是系统自动分配的,dup2的可以自己指定)( man 2 dup2
#include <unistd.h>
int dup2(int oldfd, int newfd);
oldfd 需要被复制的文件描述符。
newfd 指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。
返回值: 成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd ;如果复制失败将返
-1 ,并且会设置 errno 值。
 /* 复制文件描述符 */
 fd2 = dup2(fd1, 100);
 if (-1 == fd2) {
 perror("dup error");
 ret = -1;
 goto err1;
 }
 printf("fd1: %d\nfd2: %d\n", fd1, fd2);

原子操作与竞争冒险

线程A、B都对同一个文件操作,就会出问题。原子操作可以保护文件

        所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。

1)O_APPEND 实现原子操作

        当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作,加入 O_APPEND 标志后,不管怎么写入数据都会是从文件末尾写,这样就不会导致出现“进程 A 写入的数据覆盖了进程 B 写入的数据”这种情况了。

2)pread()pwrite()(重要)

      pread()和 pwrite()都是系统调用,与 read()write()函数的作用一样,用于读取和写入数据。区别在于, pread()和 pwrite()可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数, 用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read;同理,调用 pwrite 相当于调用 lseek 后再调用 write。所以可知,使用 pread pwrite 函数不需要使用 lseek 来调整当前位置偏移量,并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
fd buf count 参数与 read write 函数意义相同。
offset 表示当前需要进行读或写的位置偏移量。
返回值: 返回值与 read write 函数返回值意义一样。
虽然 pread (或 pwrite )函数相当于 lseek pread (或 pwrite )函数的集合,但还是有下列区别:
调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);
不更新文件表中的当前位置偏移量。
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值