Unix编程笔记(一)文件I/O

常用的文件操作函数:open、read、write、lseek以及close。
read和write都是不带缓冲的I/O操作,直接调用内核中的一个系统调用。不带缓冲是指write和read函数不带缓冲,内核有缓冲区。



1. 文件描述符

  • 文件描述符是一个非负整数
  • 当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符
  • 按照惯例,文件描述符0(STDIN_FILENO)关联标准输入,1(STDOUT_FILENO)关联标准输出,2(STDERR_FILENO)关联标准错误
  • 文件描述符的变化范围是0 ~ OPEN_MAX-1,文件描述符个数是有限制的

2. open、openat 打开文件

#include <fcntl.h>
int open (const char *__path, int __oflag, ...mode_t mode);
int openat (int __fd, const char *__path, int __oflag, ...mode_t mode);
参数说明取值范围
path文件路径,或相对fd路径;path为绝对路径时fd被忽略大多数系统支持的文件名最大长度为255
fdpath相对路径所在目录描述符,当path为相对路径时有效
oflag五选一O_RDONLY :read only
O_WRONLY :write onlt
O_WRONLY :write onlt
O_EXEC :exec 至执行打开
O_SEARCH :只搜索打开(用于目录)
可选项O_WRONLY :write onlt
O_APPEND :每次写入时追加到文件末尾
O_CLOEXEC :把FD_CLOEXEC常量设置为文件描述符标志
O_CREATE :文件不存在则创建
O_DIRECTORY :如果path不是目录则出错
O_EXCL :如果同时指定了O_CREAT,而文件不存在则出错
O_NOCTTY :如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端
O_NOFOLLOW:如果path引用的是一个符号链接,则出错
O_NONBLOCK:如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则将此选项为文件的本次打开操作和后续操作设置非阻塞方式
O_SYNC :同步,使每次write等待物理I/O完成,等待文件属性更新
O_TRUNC :如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0
O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标注termios参数值
O_DSYNC :每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不等待文件属性更新
O_RSYNC :使每个以文件描述符为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成
返回值返回最小的未使用描述符数值
// open.c
#include <fcntl.h>
#include <stdio.h>

void main()
{
    int fd = open("/mnt/e/learning/unix_c/file_io/open.c", O_RDONLY);
    if (fd < 0)
    {
        printf("open failed, return %d\r\n", fd);
    }
    else
    {
        printf("open success, fd = %d\r\n", fd);
    }
}

3. creat 创建或打开可读文件

open可以指定O_CREAT选项,当文件不存在时创建文件;还有一种方式就是使用create。

#include <fcntl.h>
int creat (const char *__path, mode_t __mode);
// 等价于 open(path,O_WRONLY | O_CREATE | O_TRUNC,mode)

mode参数是在创建文件时添加文件权限,与chmod操作类似。

  • 在文件存在是,以只读方式打开,并将文件截断为0(清空文件内容)
  • 文件不存在时,创建新文件,并以只读方式打开,设置为mode文件访问权限
// creat.c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_SIZE 100

/*
creat测试:
    1. 带参数,使用creat创建文件,并写入数据,程序终止后使用cat打印文件内容
    2. 不带参数,使用creat打开文件,程序终止后使用cat打印文件内容
*/

void main(int argc, char *argv)
{
    char *path = "/mnt/e/learning/unix_c/file_io/creat_test.txt";
    if (argc > 1)
    {
        char buf[BUFFER_SIZE];
        int fd = creat(path, 0777); // 创建文件
        sprintf(buf, "current file created by creat function,current fd = %d\r\n", fd);
        write(fd, buf, strlen(buf)); // 写入文件
    }
    else
    {
        creat(path, 0777); // 打开已经存在的文件,清空已有数据
    }
}

在这里插入图片描述

4. close 关闭文件描述符

可调用close函数关闭打开的文件。关闭文件时,还会释放该进程加在该文件上的所有记录锁
当进程终止时,内核自动关闭它所有打开的文件,利用这一功能可以不显式调用close关闭文件。

#include <unistd.h>
int close(int fd);

测试:

// close.c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

void main()
{
    printf("start close STDOUT_FILENO\n"); // 使用标准输出
    close(STDOUT_FILENO);                  // 关闭标准输出
    printf("STDOUT_FILENO closed\n");      // 使用标准输出,已经关闭了标准输出,将不会有字符串输出

    int fd = open("/mnt/e/learning/unix_c/file_io/close_test.txt", O_RDWR | O_CREAT);   // 打开新文件,fd = STDOUT_FILENO,相当于将标准输出关联到fd
    printf("open file fd = %d\r\n", fd);    // 标准输出,已经重定向到fd,相当于输出到fd
}

测试结果:
在这里插入图片描述

5. lseek 设置文件offset

  • 每个打开文件都有一个与其关联的偏移量
  • 通常write和read都是从当前偏移量位置开始
  • 当打开(open、creat)文件没有设置O_APPEND时,默认打开的文件偏移量是0,读写都是从当前文件偏移量开始的
  • lseek仅将当前的文件偏移记录在内核中,并不引起I/O操作
  • 偏移量大于当前文件长度时引起空洞
#include <unistd.h>
off_t lseek (int __fd, __off_t __offset, int __whence)	//off_t long
fd      :设置偏移量的文件描述符
offset  :偏移量
whence  :相对位置
            SEEK_SET 距文件开始位置
            SEEK_CUR 距文件当前位置
            SEEK_END 距文件末尾位置
成功则返回新的偏移量,-1 不能设置偏移量

6. read 读文件

从打开文件中读取数据;如果成功返回读到的字节数;如已到达文件的尾端,则返回0。

#include <unistd.h>
ssize_t read (int __fd, void *__buf, size_t __nbytes);
  • 读普通文件,返回读取到的数据长度,0 到了文件尾端,-1 无权限
  • 从终端设备读,通常最多一次只能读一行
  • 从管道或FIFO读,返回实际可用的字节数
  • 从面向记录的设备(如磁带)读,一次最多返回一个记录

7. write 写文件

向打开的文件写入数据。

#include <unistd.h>
ssize_t write (int __fd, const void *__buf, size_t __nbytes);

返回值通常与nbytes相同,否则出错,常见原因是磁盘已满,或者超过了一个给定进程的文件长度限制

8. I/O效率

复制一个文件的内容到另一个文件,如何才能速度最快?是一次性读取全部内容,再写入文件;还是读一个字节写一个字节;或是其他?

使用time获取程序执行时间:

/*
测试缓冲区长度对文件读写的影响

大多数文件系统为改善性能都采用某种预读技术,当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据。

每次读取不同长度的数据,测试全部读完文件的取时间长度

时间有三种:
时钟时间(总耗时)real time
用户CPU时间 user cpu time
系统CPU时间 sys cpu time

时间获取方式:
    time ./a.out

测试方式:
    time ./a.out [buffer_size]
    调整buffer_size,看时间大小
*/

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

void main(int argc, char *argv[])
{
    int n;
    int size = 4096;

    if (argc > 1)
    {
        size = atoi(argv[1]);
    }

    char *buf = (char *)malloc(size);
    char *path = "/mnt/e/BaiduNetdiskDownload/marry_photo_video.mp4";	// 指定一个大文件

    close(STDIN_FILENO); // 关闭标准输出文件描述符 0
    int fd = open(path, O_RDONLY); // 返回最小的未被使用的文件描述符 0 ,相当于将STDIN_FILENO重定向
    printf("fd = %d\r\n", fd);

    int fd3 = dup(STDOUT_FILENO);   // 复制现有的文件描述符
    close(STDOUT_FILENO);
    int fd2 = open("/dev/null", O_WRONLY); // 重定向STDOUT_FILENO

    printf("print fd2 = %d \r\n", fd2); // 将不会得到输出,因为STDOUT_FILENO已经重定向到/dev/null

    // int fd3 = open("/dev/stdout", O_WRONLY);
    char tbuf[100];
    sprintf(tbuf, "fd2 = %d", fd2);
    write(fd3, tbuf, strlen(tbuf)); // 输出到stdout

    while ((n = read(STDIN_FILENO, buf, size)) > 0)
    {
        if (write(STDOUT_FILENO, buf, n) != n)
        {
            printf("write error\n");
        }
    }

    if (n < 0)
        printf("write error\n");
}

结果:
在这里插入图片描述

9. 文件共享(内核用于I/O的数据结构)

进程表项、文件表项、v节点表项

在不同进程中共享打开的文件

文件描述符表:
    文件描述符标志、指向文件表项的指针


进程表项(进程):
    进程内的记录项,包含一张打开文件描述符表(包含文件描述符标志、指向文件表项的指针)

文件表项(内核):
    包含
        文件状态标志(读、写、同步和非阻塞等)
        文件偏移量(lseek修改文件偏移量,不是I/O操作)
        指向该文件v节点表项的指针

V节点表项:
    包含
        文件类型及该类型的各种操作函数的指针
        i节点(文件所有者,文件长度,指向文件实际数据块在磁盘上所在位置的指针)

每个进程维护自己的进程表项(文件描述符表)
多个进程打开同一个文件时,每个进程获得各自的文件表项,并指向同一个V节点表项
可能有多个文件描述符指向同一个文件表项(共享文件表项)


用O_APPEND打开一个文件,每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。
以O_APPEND打开的文件操作是原子操作

多个进程读取同一个文件时,可能产生预想不到的结果(非原子操作)

两个进程打开同一个文件的情况:
在这里插入图片描述

10. 原子操作

10.1 追加到一个文件

如果两个进程同时打开同一个文件,并同时将数据写入文件,如何保证数据不被覆盖?

在每次写入数据之前,将当前offset设置到文件尾端处,是否可以实现?代码如下:

lseek(fd,0,SEEK_END);	// 设置文件offset值文件尾端
write(fd,buf,strlen(buf));	// 写入数据

好像是可以,但是如果在lseek与write之间有另一个进程写入了数据到文件尾端,write时将会覆盖其他线程写入的数据,并不能保证写入的原子性。lseek和write合成的操作并不是原子操作!

O_APPEND:在打开文件时设置O_APPEND标志,内核在每次写操作之前都将进程的当前偏移量设置到文件的尾端处。以O_APPEND打开的文件写入文件时是原子操作,每次都会将数据写入到文件尾端。

10.2 pread、pwrite

原子性定位并执行I/O。

#include <unistd.h>
ssize_t pread(int fd,void * buf,size_t nbytes,off_t offset);
ssize_t pwrite(int fd,const void * buf,size_t nbytes,off_t offset);

pread 等价于 lseek + read,但是pread是原子操作,不更新当前文件偏移量
pwrite相当于 lseek + write,但也是原子操作

11. dup、dup2 复制现有文件描述符

复制一个现有的文件描述符。

#include <unistd.h>
int dup (int __fd);
int dup2 (int __fd, int __fd2);	// 指定新的文件描述符为fd2

示例:

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

void main()
{
    int fd = dup(STDOUT_FILENO); // 复制标准输出文件描述符

    char buf[100];
    sprintf(buf, "this is output from fd = %d\r\n", fd);
    write(fd, buf, strlen(buf));    // 输出字符串到标准输出
}

12. sync、fsync、fdatasync 从内核缓冲区写入队列或磁盘

#include <unistd.h>
int sync();
int fsync(int fd);
int fdatasync(int fd);

文件写入的顺序是:内核缓冲区 => 队列 => 磁盘,通常write之后数据保存在内核缓冲区,系统守护进程(update)周期性的调用(一般间隔30s)sync函数。

sync : 将内核缓冲区写入队列,对全部内核缓冲区起作用;
fsync:将内核缓冲区数据写入磁盘操作结束后才返回,同步更新文件的属性,只对指定的fd文件起作用;
fdatasync:将内核缓冲区数据写入磁盘操作后才返回,但是只影响数据部分(比fsync少了文件属性更新),只对指定的fd文件起作用;

内核缓冲区 队列 磁盘 sync fsync/fdatasync 内核缓冲区 队列 磁盘

13. fcntl 文件属性操作

fcntl函数可以改变已经打开文件的属性。

#include <fcntl.h>
int fcntl(int fd,int cmd,... /* arg */);

fcntl有以下5中功能:

  • (1) 复制一个已有的文件描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)
  • (2) 获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD)
  • (3) 获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)
  • (4) 获取/设置一部I/O所有权(cmd=F_GETTOWN或F_SETTOWN
  • (5) 获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)

14. ioctl I/O操作

ioctl函数是I/O操作的杂物箱,所有I/O都能用ioctl实现。终端I/O是使用ioctl最多的地方。

#include <unistd.h>	/* System V */
#include <sys/ioctl.h> /* BSD and Linux */
int ioctl(int fd,int requset, ...);

每个设备驱动程序可以定义它自己专用的一组ioctl命令,系统则为不同种类的设备提供通用ioctl命令。

15. /dev/fd

较新的系统都提供名为/dev/fd/的目录,其目录项是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符n
某些系统提供路径名/dev/stdin、/dev/stdout和/dev/stderr,这些等效于/dev/fd/0、/dev/fd/1、/dev/fd/2。
/dev/fd文件主要由shell使用。

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
通过/dev/fd/1写到标准输出
*/
void main()
{
    int fd = open("/dev/fd/1", O_RDWR);
    char buf[100];
    sprintf(buf,"this is output from /dev/fd/1, fd = %d\r\n",fd);
    write(fd,buf,strlen(buf));
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值