系统调用IOopen、close、read、write、lseek

目录

  • 3 系统调用IO
    • 3.1 文件描述符
      • 3.1.1 FILE结构体
      • 3.2.2 文件描述符
    • 3.3 open、close、read、write、lseek
      • 3.3.1 文件权限
      • 3.3.2 open
      • 3.3.3 close
      • 3.3.4 read
      • 3.3.5 write
      • 3.3.6 lseek
      • 3.3.7 代码示例
  • 文件io和标准io的区别(效率)

橙色

3 系统调用IO

3.1 文件描述符

3.1.1 FILE结构体

查看stdio.h头文件中,有FILE结构体的定义:

//stdio.h
typedef struct _iobuf {
    char*  _ptr;        //文件输入的下一个位置
    int    _cnt;        //当前缓冲区的相对位置
    char*  _base;       //文件初始位置
    int    _flag;       //文件标志
    int    _file;       //文件有效性
    int    _charbuf;    //缓冲区是否可读取
    int    _bufsiz;     //缓冲区字节数
    char*  _tmpfname;   //临时文件名
} FILE;

其中_file就是文件描述符。

3.2.2 文件描述符

文件描述符(fd,file descriptor)是文件IO(也系统IO)中贯穿始终的类型。如下图所示:

3.3 open、close、read、write、lseek

3.3.1 文件权限

关于文件权限的相关内容请参考该篇博客:【Linux】用户管理(添加用户、修改密码、删除用户、查询用户信息、切换用户、查看当前用户、用户组)

另外补充一个知识点:

umask

Linux具有默认权限:

  • 一个目录被创建,默认权限是drwxrwxrwx,即777
  • 一个普通文件被创建,默认权限是-rw-rw-rw-,即666

但实际上所创建的文件和目录,看到的权限往往不是上面这个值。原因就是创建文件或目录的时候还要受到 umask 的影响。umask值表明了需要从默认权限中去掉哪些权限来成为最终的默认权限值。

在这里插入图片描述

3.3.2 open

open用于打开或创建一个文件或者设备。

所在头文件:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

函数原型1:

int open(const char *pathname, int flags);
  • 将准备打开的文件或是设备的名字作为参数path传给函数,flags用来指定文件访问模式。
  • open系统调用成功返回一个新的文件描述符,失败返回-1。

其中,flags是由必需文件访问模式和可选模式一起构成的(通过按位或|):
在这里插入图片描述


函数原型2:

int open(const char *pathname, int flags, mode_t mode);

在第一种调用方式上,加上了第三个参数mode,主要是搭配O_CREAT使用,这个参数规定了用户、同组用户和其他人对文件的文件操作权限。只列出部分:
在这里插入图片描述
例如:

int fd = open("./file.txt",O_WRONLY | O_CREAT, 0600);

创建一个普通文件,权限为0600,拥有者有读写权限,组用户和其他用户无权限。

3.3.3 close

close:关闭一个文件描述符

#include <unistd.h>

int close(int fd);

返回 0 表示成功,或者 -1 表示有错误发生,并设值errno;

3.3.4 read

read所在头文件和函数原型:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • 从与文件描述符fd相关联的文件中读取前count字节的内容,并且写入到数据区buf中
  • read系统调用返回的是实际读入的字节数,发生错误返回-1

3.3.5 write

write所在头文件和函数原型:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • 把缓存区buf中的前count字节写入到与文件描述符fd有关的文件中
  • write系统调用返回的是实际写入到文件中的字节数,发生错误返回-1,注意返回0不是发生错误,而是写入的字节数为0

3.3.6 lseek

lseek所在头文件和函数原型:

#include <sys/types.h>
#include <unistd.h>

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

lseek设置文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。

whence取值:

字段 含义
SEEK_SET 文件开头
SEEK_END 文件末尾
SEEK_CUR 文件当前位置

3.3.7 代码示例

一个copy的代码:

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

int main(int argc,char** argv){
    int len,ret,pos;
    int fds,fdd;
    char buf[BUFFSIZE];
    if(argc<3){
        fprintf(stderr,"input error");
        exit(1);
    }
    fds=open(argv[1],O_RDONLY);
    if(fds<0){
        perror("open");
        exit(1);
    }
    fdd=open(argv[2],O_WRONLY|O_CREAT,O_TRUNC,0600);
    if(fdd<0){
        close(fds);
        perror("open");
        exit(1);
    }

    while(1)
    {
        len=read(fds,buf,BUFFSIZE);
        if(len<0)
        {
            perror("read");
            break;
        }
        if(len==0)
        {
            break;
        }
        pos=0;
        while(len >0)
        {
            ret=write(fdd,buf+pos,len);
            if(ret<0)
            {
                perror("write");
                exit(1);
            }
            len-=ret;
            pos+=ret;
        }

    }

    close(fdd);
    close(fds);
    exit(0);
}

文件io和标准io的区别(效率)

  • 文件I/O:文件I/O又称为无缓冲IO,低级磁盘I/O,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。

  • 标准I/O:标准I/O是ANSI C建立的一个标准I/O模型,又称为高级磁盘I/O,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存(行缓存、全缓存和无缓存)。

Linux 中使用的是GLIBC,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O。

缓存是内存上的某一块区域。缓存的一个作用是合并系统调用即将多次的标准IO操作合并为一个系统调用操作。

文件IO不使用缓存,每次调用读写函数时,从用户态切换到内核态,对磁盘上的实际文件进行读写操作,因此响应速度快,坏处是频繁的系统调用会增加系统开销(用户态和内核态来回切换),例如调用write写入一个字符时,磁盘上的文件中就多了一个字符。

标准IO使用缓存,未刷新缓冲前的多次读写时,实际上操作的是内存上的缓冲区,与磁盘上的实际文件无关,直到刷新缓冲时,才调用一次文件IO,从用户态切换到内核态,对磁盘上的实际文件进行操作。因此标准IO吞吐量大,相应的响应时间比文件IO长。但是差别不大,建议使用标准IO来操作文件。

文件io(系统调用io)响应速度快,它在输出端没有缓冲区,有数据来了,就直接处理;标准io吞吐量更大,因为它有缓冲区,所以是等满足一定条件了,再把缓冲区中的数据一起发送出去。
从实际的用户体验来说,吞吐量大会感觉更快。所以在文件io和标准io都能够调用的时候,选择标准io是更好的。


两种IO可以相互转化:
fileno:返回结构体FILE的成员_file,即文件描述符。标准IO->文件IO

int fileno(FILE *stream);

fdopen:通过文件描述符fd,返回FILE结构体。文件IO->标准IO

FILE *fdopen(int fd, const char *mode);

提醒:即使对同一个文件,也不要混用两种IO,否则容易发生错误。

一个小例子:

#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#define BUFFSIZE 1024

int main(){
    putchar('a');
    write(1,"b",1);

    putchar('a');
    write(1,"b",1);

    putchar('a');
    write(1,"b",1);

    exit(0);
}

输出结果:bbbaaa

很好的印证了上面所说的标准IO和系统IO的区别,write是系统IO,所以立即输出;而putchar是标准IO,有缓冲区,并未立即输出。

BUFSIZE对IO效率的影响
在这里插入图片描述
图中用户CPU时间是程序在用户态下的执行时间;系统CPU时间是程序在内核态下的执行时间;时钟时间是两个时间的总和;

BUFSIZE受栈大小的影响;此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4096字节。这也证明了图中系统 CPU 时间的几个最小值差不多出现在BUFFSIZE 为4096 及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。