Linux C 系统编程 1-1 文件与I/O 基本操作

该系列文章总纲链接:专题分纲目录 LinuxC 系统编程


本章节思维导图如下所示(思维导图会持续迭代):

第一层:

第二层:


计算机的两个重要概念:时间与空间。

  1. 时间:计算机将其抽象为进程。
  2. 空间:计算机将其抽象为文件。

1 文件I/O基本操作

文件描述符是进程打开文件的桥梁,通过这个桥梁进程才能够对文件进行操作。在linux环境下每个磁盘文件打开时都会在内核中建立一个文件表项,这个文件表项包括文件的状态信息、存储文件内容的缓冲区以及当前文件位置的读写等,当同一个文件打开两次时会创建两个这样的文件表项,读写该文件只会影响到该文件表项的读写位置,这些文件表项共同保存在内核的数组里,这个数组就是文件表。每个进程在内核中都保存一个整型数组,该数组的每个元素是文件表的下标。因此使用下标就可以引用打开的文件表项了。该数组下标就是文件描述符,每个进程中的文件表下标的数组就是文件描述符数组。当进程需要引用文件时,只需要引用这个文件描述符就可以。

1.1 打开文件 open

open函数的原型:

#include <fcntl.h>
int open(const char* pathname, int flags);
int open(const char* pathname, int flags,mode_t mode);
参数pathname: 要打开的文件所在的路径(绝对路径/相对路径)。
参数flags:打开文件的模式,值如下:
     必要参数:
                    O_RDONLY     :只读方式          0
                    O_WRONLY     :只写方式          1
                    O_RDWR          :可读可写方式     2
     可选参数:
                    O_APPEND     :所写入的数据会以附加的方式加入到文件后面
                    O_CREAT          :没有此文件则创建    
                    O_EXCL          :如果设置了O_CREAT且文件应尽存在则报错
                    O_TRUNC          :如果文件存在且以写的方式成功打开,则文件截短为0。
                    O_NOTTY          :如果指定的是终端设备,则不把控制终端分配给调用open函数的进程。
                    O_NONBLOCK     :如果指定的设备是管道、块设备/字符设备,则此设备设置为阻塞。
     其他参数:
          同步输入输出参数:
                    O_DSYNC          :等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
                    O_RSYNC          : read 等待所有写入同一区域的写操作完成后再进行
                    O_SYNC          : 等待物理 I/O结束后再 write,包括更新文件属性的 I/O     
     对于flags,需要必要参数与可选参数以及其他参数进行或操作取得最后的值。    
参数mode:打开的文件不存在,需要创建新文件的时候,新文件的权限(注意:创建新文件的时候,新文件的权限受到umask的影响)。一般情况下,只要open函数需要创建文件,都要给出mode的值,不要默认,因为默认值是一个随机值。
open的返回值是一个文件描述符,但是此文件描述符一定是当前进程中可用文件描述符中最小的一个。
对于open发生错误,一般用perror来进行容错处理,返回的结果一般有两种:
no such file 和 permission denied。

1.2 创建文件 creat

creat函数的原型:

#include <fcntl.h>
int creat(const char* pathname,mode_t mode);
参数pathname: 要打开的文件所在的路径(绝对路径/相对路径)。
参数mode等价于open函数的第三个参数,即所要创建文件的权限。
creat函数等价于open(const char* pathname,O_CREAT|O_TRUNC|O_WRONLY,mode_t mode);

1.3 关闭文件 close

close函数的原型:

#include <unistd.h>
int close(int fd);
参数fd:要关闭的文件的文件描述符。
此函数成功返回1,失败返回-1。
close一般很少发生错误,但是在极限情况下检查close函数的返回值是很必要的,尤其是在处理网络问题的时候。
close函数在关闭的时候会将内存缓冲区中的内容写在外存上,关闭一个网络文件相当于与对端进行一次数据通讯,这时出错的概率是很大的,因为网络中随时可能会丢包。一般采用循环机制来处理这种问题,即只要返回不成功就一直close。

1.4 文件定位 lseek

lseek函数的原型:

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数fd:所要操作文件的文件描述符。
参数offset:相对于whence的偏移量,可以是正数、负数、0。
参数whence:
    SEEK_SET     :文件起始位置          0
    SEEK_CUR     :文件当前位置          1
    SEEK_END     :文件结束位置          2
注意:linux下不是所有文件都可以进行文件定位操作的,例如套接字文件和管道是不可以进行文件定位的。

1.5 文件截短与清空 truncate与ftruncate

truncate与ftruncate函数原型:

#include <unistd.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
参数path     :文件路径。
参数fd       :已经打开的文件的文件描述符。
参数length   :截短的长度,超过该长度部分的数据将会被系统自动遗弃。如果文件的长度小于length的值则系统会自动扩展空间,形成文件空洞,之后会向空洞中写入数据。
truncate用于截短文件,ftruncate用于截短已经打开的文件。
truncate与ftruncate均是成功返回0,失败返回-1。
对于清空操作,只需要将文件截短为0即可。

1.6 文件读写操作 read和write

read和write的函数原型:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
参数fd        :已经打开的文件的文件描述符。
参数buf       :将文件内容读入到该缓冲区/将缓冲区内容写入文件
参数count     :读/写的大小。
read函数的返回值是实际读入缓冲区中的字节数。失败返回-1。如果读取的文件太大,则需要分批读入。read不会在读入的内容中加入“\0”字符,需要自己根据需要添加。
write函数的返回值是实际写入文件的字节数。失败返回-1。

1.7 文件同步 fsync、fdatasync与sync

文件同步:由于文件的写操作会由于缓冲区的缘故致使输出延时,所以在一段时间内,会导致内存中的内容和外存中的内容不一致,为了避免那这种情况,用户可以指定系统在缓冲区为填满的情况下将缓冲区中的内容写入到磁盘文件中去。
fsync、fdatasync与sync函数的原型:

#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
参数fd          :已经打开的文件的文件描述符。
sync:将打开的文件写回到磁盘上,将修改过的内容放入系统队列后返回,并不等待盘块实际写入外存,因此该函数能加快文件同步速度,但是并没有真正地保证文件的同步。
fsync:确保文件的实际写入,该函数会阻塞直到修改的盘块写到外存后才返回。成功返回0,失败返回-1。
fdatasync:与fsync函数的参数和返回值意义是一样的;但是功能与sync函数类似,不同的是fdatasync只更新文件的数据部分,不更新文件的属性部分。         

2 文件描述符的操作

不操作文件本身,而是操作文件与进程的链接点。

2.1 复制文件描述符 dup与dup2

dup与dup2的函数原型:

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
参数oldfd     :需要复制的文件描述符
参数newfd     :指定要复制到的文件描述符
对于dup而言,复制后的文件描述符共用一个文件表项,所以两个文件的偏移量、状态标志等都是共享的。返回值是一个新的文件描述符。dup函数总是找到进程文件表中的第一个可用的文件描述符,将参数指定的文件描述符复制到该描述符后,返回这个描述符。它的模型类似于指针,指向同一个地址。
对于dup2(实际含义是 dup to)而言,与dup类似,但是允许用户指定将文件描述符复制到哪个文件描述符上。
dup2(fd1,fd2);返回值是fd2,如果fd2所对应的文件已经打开则关闭后再将fd1复制给fd2。
注意:fd = dup2(fd1,fd1);这种语句是很危险的,在复制时,已经关闭了fd1所代表的文件,这时复制过来的fd1文件描述符将是一个无意义的值,对其操作会导致程序的错误。所以在使用dup2时应注意容错:
     if(fd1!=fd2)
          fd = dup2(fd1,fd2);
复制文件描述符的意义:多个进程对同一文件进行操作时,不需要open就能对文件进行操作,减少系统开销。

2.2 I/O重定向      <     >

<filename:输入重定向。
>filename:输出重定向。

对于输入输出重定向的使用,使得程序不必再关心文件操作的细节,变成难度降低,但是缺点也很明显,主要在2方面:

  1. 输入过于复杂,尤其是对于多个命令行参数,过多的命令参数使程序的使用复杂度增加,但写一个shell就可以解决。
  2. 过于依赖shell,如果是在某些没有shell的嵌入式系统中,这种方法不可以使用,解决的办法是利用C函数库/linux系统函数库在函数中实现功能。    

2.3 控制文件 fcntl

fcntl的函数原型:

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
参数fd          :已经打开的文件的文件描述符。
参数cmd         :fcntl可以执行的命令,一般是宏,每个宏代表不同的功能。
     F_DUPFD   :复制文件描述符          。第三个参数:复制的新文件描述符
     F_GETFD   :获取文件描述符标志     。第三个参数:无
     F_SETFD   :设置文件描述符标志     。第三个参数:新文件描述符标志
     F_GETFL   :获取文件状态标志     。第三个参数:无
     F_SETFL   :设置文件状态标志     。第三个参数:新文件状态标志
     F_GETOWN:获取异步I/O所有权     。第三个参数:无
     F_SETOWN:设置异步I/O所有权     。第三个参数:进程ID
     F_GETLK   :获取记录锁               。第三个参数:无
     F_SETLK   :设置记录锁               。第三个参数:无
     F_SETLKW  :设置记录锁               。第三个参数:无
     若命令返回失败则均返回-1,成功则返回第三个参数。
对于命令F_DUPFD有:
     调用dup(oldfd);等效于fcntl(oldfd, F_DUPFD, 0);
     调用dup2(oldfd, newfd);等效于close(oldfd);fcntl(oldfd, F_DUPFD, newfd);
参数...          :此参数的个数以及含义由第二个命令参数来决定。linux下大部分的设备也是采用这种方法。
fcntl主要是对一个已经打开的文件进行属性的获取和修改,几乎所有的属性操作都可以用fcntl函数来实现,可以将它看作一个文件操作的集合。
在修改打开文件的文件状态时,有几项属性是不能被改变的:O_RDONLY、 O_WRONLY、 O_RDWR属性不可以被fcntl函数改变。

2.4 控制文件 ioctl

它用于控制I/O设备 ,提供了一种获得设备信息和向设备发送控制参数的手段,主要向设备发控制和配置命令 。有些命令需要控制参数,这些数据是不能用read / write 读写的,称为Out-of-band数据。也就是说,read / write 读写的数据是in-band数据,是I/O操作的主体,而ioctl 命令传送的是控制信息,其中的数据是辅助的数据。

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。

ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就能在用户程序中使用ioctl函数控制设备的I/O通道。
ioctl函数的原型:

#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
参数d           :已经打开文件的文件描述符
参数request     :用户程序对设备的控制命令
参数...         :根据request而产生的常见类型/结构体类型的值结果参数,一般采用地址的方式来获取。
函数成功执行返回0,失败返回-1。

3 非阻塞I/O

操作系统通过I/O进行操作,而被操作的这些设备可能是高速设备,也可能是低速设备;速度较慢的设备会造成阻塞而影响高速设备,因此要设置非阻塞的方式来协调整个系统。当以非阻塞方式打开一个文件时,如果遇到读写的外部设备不可用,I/O操作不会阻塞,而是返回,将errno的值置为EAGAIN。该错误号表示I/O系统调用没有阻塞等待数据,导致本次读写操作失败。

非阻塞调用出错时有很多错误号为EAGAIN的出错提示信息。

4 内存映射I/O

4.1 内存映射的概念

提出内存映射是为了解决读写文件的效率问题,如果一个程序需要大量的磁盘I/O时,内存映射I/O能使程序执行的速度有很大的提高,内存映射I/O实际上是一种空间换取时间的一种机制。当用户对文件进行I/O操作时,通常需要一个内核缓冲区,输入输出的内容经过缓冲区和外部设备发生数据交换。如果需要写一个大于该缓冲区大小的文件则需要多次与外部设备进行交互,从而导致运行速度的损失,因此提出内存映射的方案。

内存映射:将磁盘文件与内存中的一个缓冲区建立一个映射关系。实现了对缓冲区进行操作就是对文件进行操作的方案。

4.2 内存映射的创建 mmap

mmap函数的原型:

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
参数addr     :建立映射的起始地址。一般置为null,表示自动选择内存映射的起始地址。
参数length     :映射磁盘文件的长度,单位是字节。
参数prot     :内存空间的访问权限。
     PROT_READ     :映射区可读
     PROT_WRITE     :映射区可写
     PROT_EXEC     :映射区可执行
     PROT_NONE     :映射区不可访问
     注意:映射区的访问权限要和所对应的文件的权限相协调,因此应该先了解文件的打开方式。
参数flags     :映射区域的属性。
     MAP_FIXED     :很少用,对拥有内存保护的操作系统基本不用。
     MAP_PRIVATE     :对映射区域的改动不反应到磁盘上,通常用于只读的映射区域,防止用户对其进行写操作。
     MAP_SHARED     :对映射区域的改动反映到磁盘上,同时该映射区读写也是自由的。
参数fd          :进行内存映射的文件的文件描述符。必须是一个已经打开了的文件描述符。
参数offset     :磁盘文件的起始映射位置在文件中的偏移值。
mmap函数执行成功返回映射区域的首地址,失败则返回宏MAP_FAILED的值。

4.3 内存映射的撤销 munmap
munmap函数的原型:

#include <sys/mman.h>
int munmap(void *addr, size_t length);
参数addr     :撤销映射的起始地址。
参数length   :映射磁盘文件的长度,单位是字节。
munmap函数执行成功返回0,失败返回-1。
当一个映射被撤消后,如果其属性为MAP_SHARED,其内容写入磁盘的时间依赖于系统守护进程的调度。

4.3 内存映射同步 msync

由于使用内存映射同样无法保证修改的内容及时写到磁盘上,每次将修改过的内存页面写回磁盘依赖于系统内部的内存换页机制。因此,如果将修改的内容及时写回需要调用特殊的系统调用。msync就是将修改过的页面写回到磁盘文件。

msync函数的原型:

#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
参数addr     :需要回写磁盘的内存映射地址。
参数length     :需要回写的字节数。
参数flags     :回写的标志
     MS_ASYNC          :不等待页面写回磁盘,只是将修改过的页面放入内核的写入队列中。
     MS_SYNC               :等待页面写回磁盘,知道页面写磁盘完毕,msync函数才返回。最常用的选项。
     MS_INVALIDATE     :丢弃指定范围内的所有页面。
函数执行成功返回0,失败返回-1。

4.4 更改内存映射的权限 mprotect

当映射区已经建立时,如果要更改一个映射区的权限,需要mprotect函数。mprotect函数的原型:

#include <sys/mman.h>
int mprotect(const void *addr, size_t len, int prot);
参数addr     :该映射区域的首地址。
参数len          :该映射区域的大小。
参数prot     :新映射区域的权限
     PROT_READ     :映射区可读
     PROT_WRITE     :映射区可写
     PROT_EXEC     :映射区可执行
     PROT_NONE     :映射区不可访问
函数执行成功返回0,失败返回-1。

5 关于I/O流

基于I/O流的缓冲区有3种:

  • 全缓冲:直到缓冲区填满才调用I/O函数
  • 行缓冲:直到遇到\n才调用I/O函数
  • 无缓冲:无缓冲区,直接将数据写道设备上

以上3个缓冲区分别定义3个宏,定义如下:

  • 全缓冲:_IO_FULL_BUF
  • 行缓冲:_IO_LINE_BUF
  • 无缓冲:_IO_UNBUFFERED

将文件流中的缓冲区标志与这些宏进行位&操作,判断结果是否为0就能知道该缓冲文件流的缓冲区类型。

例如: (stdin->_flag &宏)不为0表示是这种缓冲区,为0表示不是这种缓冲区

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图王大胜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值