二、文件IO

二、文件IO

文件描述符的概念
(实质是一个整形数,文件描述符优先使用可用范围内最小的)

文件IO操作:

fileno(3)

 #include <stdio.h>
 
 int fileno(FILE *stream);
 
    Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
 
        fileno(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

这个函数的作用是从 STDIO 的 FILE 结构体指针中获得 SYSIO 的文件描述符。

fdopen(3)

 #include <stdio.h>

 FILE *fdopen(int fd, const char *mode);
 
   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
 
      fdopen(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

这个函数和上面的 flieno(3) 函数的功能是反过来的,作用是把 SYSIO 的文件描述符转换为 STDIO 的 FILE 结构体指针。mode 参数的作用与 fopen(3) 中的 mode 参数相同,这里不再赘述。

虽然这两个函数可以在 STDIO 与 SYSIO 之间互相转换,但是并不推荐对同一个文件同时采用两种方式操作。因为 STDIO 和 SYSIO 之间它们处理文件的私有数据是不同步的,如果同时使用两种方式操作同一个文件则可能带来不可预知的后果,

open(2)

open - open and possibly create a file or device
 
 #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);

想要使用 SYSIO 操作文件或设备,要先通过 open(2) 函数获得一个文件描述符。注意博文中在函数上方标识出来的头文件,大家在使用这个函数的时候一定要一个不少的全部包含到源代码中。

  1. 参数列表:

    pathname:要打开的文件路径。

    flags:指定文件的操作方式,多个选项之间用按位或( | )运算符链接。

    必选项,三选一:O_RDONLY, O_WRONLY, O_RDWR

    可选项:可选项有很多,这里只介绍常用的,想要查看完全的可选项,可以查阅 man 手

册。

选项说明
O_APPEND追加到文件尾部。
O_CREAT创建新文件。
O_DIRECT最小化缓冲。关于缓冲区的解释:buffer 是写操作的加速机制,cache 是读操作的加速机制。
O_DIRECTORY强调一定要打开一个目录,如果 pathname 不是目录则会打开失败。
O_LARGEFILE打开大文件的时候要加这个,会将 off_t 定义为 64 bit.。
O_NOFOLLOW如果 pathname 是符号链接则不展开,也就是说打开的是符号链接文件本身,而不是符号链接指向的文件。
O_NONBLOCK非阻塞形式。阻塞是读取不到数据时死等,非阻塞是尝试读取,无论能否读取到数据都返回。
O_TRUNC将已存在的普通文件长度截断为0(也就是将文件内容清空)。

mode:8 进制文件权限。当 flags 包含 O_CREAT 选项时必须传这个参数,否则可以不用传这个参数。当然系统在创建文件的时候不会直接这个参数,而是通过如下的公式计算得到最终的文件权限:

mode & ~(umask)

具体的 umask 的值可以通过 umask(1) 命令获得。通过这样的公式进行计算可以避免程序中创建出权限过高的文件。

close(2)

 close - close a file descriptor
 
 #include <unistd.h>
 
 int close(int fd);

关闭文件描述符。

参数是要关闭的文件描述符。注意当一个文件描述符被关闭之后就不能再使用了,虽然 fd 这个变量的值没有变,但是内核已经将相关的资源释放了,这个 fd 相当于一个野指针了。

返回值:

成功为0,失败为-1。但很少对它的返回值做校验,一般都认为不会失败。

read(2)

read - read from a file descriptor
 
 #include <unistd.h>
 
 ssize_t read(int fd, void *buf, size_t count);

这是 SYSIO 读取文件的函数,作用是从文件描述符 fd 中读取 count 个字节的数据到 buf 所指向的空间。

返回值:返回成功读取到的字节数;0 表示读取到了文件末尾;-1 表示出现错误并设置 errno。

注意 read(2) 函数与 STDIO 中的 fread(3) 函数的返回值是有区别的,fread(3) 返回的是成功读取到了多少个对象,而 read(2) 函数返回的是成功读取到的字节数量。

write(2)

 write - write to a file descriptor
 
 #include <unistd.h>
 
 ssize_t write(int fd, const void *buf, size_t count);

write(2) 是 SYSIO 向文件中写入数据的函数,作用是将 buf 中 count 字节的数据写入到文件描述符 fd 所对应的文件中。

返回值:返回成功写入的字节数;0 并不表示写入失败,仅仅表示什么都没有写入;-1 才表示出现错误并设置 errno。

注意 write(2) 函数与 STDIO 中的 fwrite(3) 函数的返回值是有区别的,fwrite(3) 返回的是成功写入了多少个对象,而 write(2) 函数返回的是成功写入的字节数量。

利用文件IO read、write实现mycpy例子

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define BUFSIZE 1024

/**
 * r -> O_RDONLY
 * r+ -> O_RDWR
 * w ->  O_WRONLY|O_CREAT|O_TUNC
 * w+ -> O_RDWR|O_TRUNC|O_CREAT
 *
 */
int main(int argc, char **argv)
{
    int sfd, dfd;
    char buf[BUFSIZE];
    int len, ret;
    int pos;
    if (argc < 3)
    {
        fprintf(stderr, "Usage....\n");
        exit(1);
    }

    sfd = open(argv[1], O_RDONLY);
    if (sfd < 0)
    {
        perror("open_sfd()");
        exit(1);
    }

    dfd = open(argv[2], O_WRONLY | O_CREAT, O_TRUNC, 0600);
    if (sfd < 0)
    {
        close(sfd);
        perror("open_dfd()");
        exit(1);
    }
    while (1)
    {
        len = read(sfd, buf, BUFSIZE);
        if (len < 0)
        {
            perror("read()");
            break;
        }
        if (len == 0) //读完数据
            break;
        pos = 0;
        while (len > 0) // 读到的字节个数
        {
            ret = write(dfd, buf + pos, len);
            if (ret < 0)
            {
                perror("write()");
                exit(1);
            }
            pos += ret;
            len -= ret;
        }
    }
    close(dfd);
    close(sfd);
    exit(0);
}

lseek(2)

 lseek - reposition read/write file offset
 
 #include <sys/types.h>
 #include <unistd.h>
 
 off_t lseek(int fd, off_t offset, int whence);

它是系统为了方便我们读写文件而设定的一个标记,随着我们通过函数对文件的读写,它会自动相应的向文件尾部偏移。

  1. 参数列表:

    fd:要操作的文件描述符;

    offset:相对于 whence 的偏移量;

    whence:相对位置;三选一:SEEK_SET、SEEK_CUR、SEEK_END

    SEEK_SET 表示文件的起始位置;

    SEEK_CUR 表示文件位置指针当前所在位置;

    SEEK_END 表示文件末尾;

返回值:

成功时返回文件首相对于移动结束之后的文件位置指针所在位置的偏移量;失败时返回 -1 并设置 errno;

这个函数的 offset 参数和返回值都对基本数据类型进行了封装,这一点要比标准库的 fseek(3) 更先进。

写一段伪代码来说明这个函数的使用方法。

 lseek(fd, -1024, SEEK_CUR); // 从文件位置指针当前位置向前偏移 1024 个字节
 lseek(fd, 1024, SEEK_SET); // 从文件起始位置向后偏移 1kb
 lseek(fd, 1024UL*1024UL*1024UL*5UL, SEEK_SET); // 产生一个 5GB 大小的空洞文件

利用lseek实现提个5G空洞文件的例子

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
    int fd;
      if(argc < 2)
    {
        fprintf(stderr, "Usage....\n");
        exit(1);
    }
    fd = open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0600);
    if(fd < 0)
    {
        perror("open() argv[1]");
        exit(1);
    }
    lseek(fd, 5LL*1024LL*1024LL*1024LL-1LL,SEEK_SET);

    write(fd,"",1);

    close(fd);

    exit(0);

}

time(1)

time(1) 命令的作用是监视一个程序的用户时间,从而可以粗略的帮助我们分析这个程序的执行效率
这是一块cp命令将/etc/services /tmp/out文件中所用的时间。

[root@VM-0-7-centos 02SYSIO]# time /bin/cp /etc/services /tmp/out

real    0m0.002s
user    0m0.000s
sys     0m0.002s

sys 是程序在内核态消耗的时间,也就是执行系统调用所消耗的时间。
user 是程序在用户态消耗的时间,也就是程序本身的代码所消耗的时间。
real 是用户等待的总时间,是 sys + user + CPU 调度时间,所以 real 时间会稍微比 sys + user 时间长一点。一个程序从提高响应素的的方式提高用户体验,一般指的就是提高 real 时间

原子操作

通俗来讲,原子操作就是将多个动作一气呵成的做完,中间不会被打断,要么执行完所有的步骤,要么一步也不会执行。这里用创建临时文件来举个栗子。

 tmpnam, tmpnam_r - create a name for a temporary file
 
 #include <stdio.h>
 
 char *tmpnam(char *s);

如果我们需要创建一个临时文件,那么首先需要又操作系统提供一个文件名,然后再创建这个文件。

tmpnam(3)

函数就是用来获得临时文件的文件名的。为什么要通过这个函数由操作系统来为我们生成文件名呢?就是因为系统中进程比较多,临时文件也比较多,怕文件重名嘛。

但是这个函数只负责生成一个目前系统中不存在的临时文件名,并不负责创建一个文件,所以创建文件的任务要由我们自己使用 fopen(3) 或 open(2) 等手段创建。

假设在我们拿到这个文件名的时候,临时文件还没有在磁盘上真正创建,另一个进程拿到了一个与我们相同的文件名,那么这个时候再创建文件就是有问题的了。

这就是因为获得文件名与创建文件这个动作不原子造成的,如果获得唯一的文件名和创建文件这个动作一气呵成中间不会被打断,则这个问题就不会发生,我们创建好文件之后另一个进程就再也拿不到相同的文件名了。

dup(2)、dup2(2)

 dup, dup2 - duplicate a file descriptor
 
 #include <unistd.h>
 
 int dup(int oldfd);
 int dup2(int oldfd, int newfd);
 

这两个函数是用来复制文件描述符的,就是 图1 中 文件描述符 3 和 6 指向了同一个文件表项的情况。

用 dup(2) 实现输出的重定向。

  #include <stdio.h>
  #include <unistd.h>
  #include <fcntl.h>
  
  #include <sys/types.h>
  #include <sys/stat.h>
  
  int main (void)
 {
     int fd = -1;
 
     fd = open("tmp", O_WRONLY | O_CREAT | O_TRUNC, 0664);
     /* if error */
 
     #if 0
     close(1); // 关闭标准输出
     dup(fd);
     #endif
     dup2(fd, 1);
     close(fd);
 
     /* 要求在不改变下面的内容的情况下,使输出的内容到文件中  */
 
     puts("dup test.");
 
     return 0;
 }
}

puts(3) 函数是将参数字符串写入到标准输出 stdout(文件描述符是 1) 中,而标准输出默认目标是我们的 shell。如果想要让 puts(3) 的参数输出到一个文件中,实现思路是:首先打开一个文件获得一个新的文件描述符,然后关闭标准输出文件描述符(1),然后使用 dup(2) 函数族复制产生一个新的文件描述符,此时的 1 号文件描述符就不是标准输出的文件描述符了,而是我们自己创建的文件的描述符了。还记得我们之前提到过吗,文件描述符优先使用可用范围内最小的。进程中当前打开的文件描述符有标准输入(0)、标准输出(1)、标准错误(2)和我们自己打开的文件(3),当我们关闭了 1 号文件描述符后,当前可用的最小文件描述符是 1,所以新复制的文件描述符就是 1。而标准库函数 puts(3) 在调用系统调用 write(2) 函数向 1 号文件描述符打印时,正好是打印到了我们所指定的文件中。

由于题目的要求是 puts(3) 上面注释以下的内容都不能修改,原则上 1 号文件描述符在这里使用完毕也需要 close(2),所以这里造成了一个内存泄漏,但并不影响对 dum(2) 函数族的解释和测试。

上面的代码用 close(2) + dup(2) 的方式或者 dup2(2) 的方式都可以实现。

dup(2) 和 dup2(2) 的作用是相同的,区别是 dum2(2) 函数可以用第二个参数指定新的文件描述符的编号。

如果新的文件描述符已经被打开则先关闭它再重新打开。

如果两个参数相同,则 dup2(2) 函数会返回原来的文件描述符,而不会关闭它。

另外一点比较重要,close(2) + dup(2) 的方式不原子,而 dup2(2) 这两步动作是原子的,在并发的情况下可能会出现问题。

**sync(2)**同步磁盘映射

 sync, syncfs - commit buffer cache to disk
 
 #include <unistd.h>
 
 void sync(void);

sync(2) 函数族的函数作用是全局催促,将 buffer 和 cache 刷新和同步到 disk,一般在设备即将卸载的时候使用。

fcntl(2)

 fcntl - manipulate file descriptor
 
 #include <unistd.h>
 #include <fcntl.h>
 
 int fcntl(int fd, int cmd, ... /* arg */ );

这是一个管家级别的函数,根据不同的 cmd 和 arg 读取或修改对已经打开的文件的操作方式。

ioctl(2)

 ioctl - control device
 
 #include <sys/ioctl.h>

 int ioctl(int d, int request, ...);

Linux 的一切皆文件的设计原理将所有的设备都抽象为一个文件,当一个设备的某些操作不能被抽象成打开、关闭、读写、跳过等动作时,其它的动作都通过 ioctl(2) 函数控制。

例如将声卡设备抽象为一个文件,录制音频和播放音频的动作则可以被抽象为对声卡文件的读、写操作。但是像配置频率、音色等功能无法被抽象为对文件的操作形式,那么就需要通过 ioctl(2) 函数对声卡设备进行控制,具体的控制命令则由驱动程序提供。

fsync 同步一个文件的data

fdatasync只刷数据不刷亚数据(亚数据是指文件的修改时间,文件的属性)

 fsync, fdatasync - synchronize a file's in-core state with storage device

SYNOPSIS
       #include <unistd.h>

       int fsync(int fd);
       int fdatasync(int fd);

/dev/fd

/dev/fd 是一个虚拟目录,它里面是当前进程所使用的文件描述符信息。如果用 ls(1) 查看,则里面显示的是 ls(1) 这个进程所使用的文件描述符信息。而打开里面的文件则相当于复制文件描述符。

文件IO与标准IO的区别

类型	  可移植性	实时性	吞吐量	功能
STDIO	  高	   低	 高	   受限
SYSIO	  低	   高	 低	   自由

这里我一个一个的解释表格中的每一项,表格中的每一项都是两者之间相对而言,使用哪种 IO 并没有绝对的好坏之分,要根据实际的需求来决定应该使用哪个。

可移植性:

标准 IO 是 C89 支持的函数,所以使用了标准 IO 的程序无论在 Linux 平台还是换成了 Windows 平台,不用修改代码是可以直接编译运行的。

而系统 IO 是由内核直接提供的函数库实现的,不同的操作系统平台上提供的 IO 操作接口是不同的,所以想要移植使用了系统 IO 的程序,必须按照目标平台的 IO 库修改程序并重新调试。

所以你写的程序将来可能在不同的平台上运行,那么最好使用标准 IO 库;如果你的程序是专门针对于某个平台而开发的,那么使用系统 IO 库能够得到我们下面说的其它优势。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值