Linux IO系统:标准C的I/O和文件I/O系统调用

1 简介

本文简要说明标准C的IO函数和Linux下的文件IO的差异。

Linux系统版本:Linux version 2.6.32-431.el6.x86_64 (mockbuild@x86-023.build.eng.bos.redhat.com) (gcc version 4.4.7 20120313 (Red Hat 4.4.7-4) (GCC) ) #1 SMP Sun Nov 10 22:19:54 EST 2013

参考视频:https://study.163.com/course/courseLearn.htm?courseId=1002913008&share=1&shareId=1024164580#/learn/video?lessonId=1003300173&courseId=1002913008

参考书籍:Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华

2 标准C的I/O

C库函数的文件操作实际上独立于具体的操作系统平台。

2.1 常用函数和结构体

2.1.1 创建和打开

FILE *fopen(const char *path, const char *mode);

fopen()用于打开指定文件filename,其中的mode为打开模式,C库函数中支持的打开模式:

其中,b用于区分二进制文件和文本文件,这一点在DOS、Windows系统中是有区分的,但Linux不区分二进制文件和文本文件。

2.1.2 读写

C库函数支持以字符、字符串等为单位,支持按照某种格式进行文件的读写,这一组函数为:

typedef struct iobuf{
    int cnt;      //剩余的字节数
    char *ptr;    //下一个字符的位置
    char *base;   //缓冲区的位置
    int flag;     //文件访问模式
    int fd;       //文件描述符
}FILE; 

int fgetc(FILE *stream);
char *fgets(char *s, int size, FILE *stream);  //整行输入
int fputc(int c, FILE *stream);
int fputs(const char *s, FILE *stream);
int printf(const char *format, …);
int fprintf(FILE *stream, const char *format, …);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

fread()实现从流(stream)中读取n个字段,每个字段为size字节,并将读取的字段放入ptr所指的字符数组中,返回实际已读取的字段数。当读取的字段数小于num时,可能是在函数调用时出现了错误,也可能是读到了文件的结尾。因此要通过调用feof()和ferror()来判断。

fwrite()实现从缓冲区ptr所指的数组中把n个字段写到流(stream)中,每个字段长为size个字节,返回实际写入的字段数。

标准I/O函数都是带缓存的。

img

stdin:标准输入,针对键盘

stdout:标准输出,针对屏幕

stderr:标准出错,针对屏幕

三个实质都是FILE类型的结构体指针,称为流指针。

2.2 标准C的IO缓存类型

(1)全缓存

要求填满整个缓冲区后才进行I/O系统调用操作,对于磁盘文件通常使用全缓存访问。

(2)行缓存

涉及一个终端时(例如标准输入和标准输出),使用行缓存;行缓存满自动溢出;碰到换行符自动输出。

(3)无缓存

标准错误流stderr通常是不带缓存的,这使得错误信息能够尽快的显示出来。

测试行缓存的案例:

 1 #include <stdio.h>
 2 
 3 int main(void)
 4 {
 5     printf("hello iotek");
 6     while (1) {
 7         sleep(1);
 8     }
 9 
10     return 0;
11 }

运行测试:

img

程序不会打印输出。

把输出改为:printf("hello iotek\n");

运行程序有输出:

img

3 文件I/O系统调用

3.1 常用函数

打开文件:open()

创建文件:create()

关闭文件:close()

读取文件:read()

写入文件:write()

文件定位:lseek()

这些不带缓存的函数都是内核提供的系统调用,他们不是ANIC C的组成部分,是POSIX的组成部分。

系统调用与C库的差异:

img

标准库函数:遵循ISO标准,基于流的I/O,对文件指针(FILE结构体)进行操作。

系统调用:兼容POSIX标准,基于文件描述符的I/O,对文件描述符进行操作。

3.2 文件描述符

对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数,当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,用open或create返回的文件描述符标识该文件,将其作为参数传给read或write。

在POSIX应用程序中,标准输入、标准输出、标准错误使用整数0、1、2表示,整数0、1、2被替换成符号常数STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO。这些常数都定义在头文件<unistd.h>中。文件描述符的范围是0-OPEN_MAX。Linux为1024。

3.3 文件描述符和文件指针之间的转换

标准文件指针:stdin 0;stdout 1;stderr 2

文件指针和文件描述符之间的转换函数:

FILE *fdopen(int fd, const char *mode); //文件描述符=>文件指针(fd=>FILE*)
int fileno(FILE *stream); //文件指针=>文件描述符(FILE *=>fd)

3.4 常用I/O系统调用函数说明

3.4.1 open函数

1 #include <sys/types.h> //头文件标准路径/usr/include
2 #include <sys/stat.h>
3 #include <fcntl.h>
4 int open(const char *pathname, int flags);
5 int open(const char *pathname, int flags, mode_t mode);

返回:若成功返回文件描述符,出错返回-1

功能:打开或创建一个文件

参数:

pathname:要打开或创建的文件路径,缺省是认为在当前路径。

flags:用来说明此函数的多个选择项,O_RDONLY只读,O_WRONLY只写,O_RDWR读写

注:用下列一个或多个常数进行或运算构成flags参数(这些常数定义在<fcntl.h>头文件中)

O_RDONLY:以只读方式打开文件

O_WRONLY:以只写方式打开文件

O_RDWR:以读写方式打开文件

O_APPEND:以追加模式打开文件,每次写时都追加到文件的尾端,但在网络文件系统进行操作时没有保证。

O_CREAT:如果指定的文件不存在,则按照mode参数指定的文件权限来创建文件。

O_EXCL:如果同时指定了O_CREAT,而文件已经存在,则出错。这可测试一个文件是否存在,但在网络文件系统进行操作时没有保证。

O_DIRECTORY:如果参数pathname不是一个目录,则open出错。

O_TRUNC:如果此文件存在,而且为只读或只写成功打开,否则将其长度截短为0。

O_NONBLOCK:如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置为非阻塞方式。

mode:新建文件的访问权限,对于open函数而言,仅当创建新文件时(flags为O_CREAT)才使用第三个参数。

mode可以是下表中的值:

image-20220105130033773

除了可以通过上述宏进行“或”逻辑产生标志以外,我们也可以自己用数字来表示,Linux用5个数字来表示文件的各种权限:第一位表示设置用户ID;第二位表示设置组ID;第三位表示用户自己的权限位;第四位表示组的权限;最后一位表示其他人的权限。每个数字可以取1(执行权限)、2(写权限)、4(读权限)、0(无)或者是这些值的和。

例如,要创建一个用户可读、可写、可执行,但是组没有权限,其他人可以读、可以执行的文件,并设置用户ID位,那么应该使用的模式是1(设置用户ID)、0(不设置组ID)、7(1+2+4,读、写、执行)、0(没有权限)、5(1+4,读、执行)即10705:

open("test", O_CREAT, 10705);

上述语句等价于:

open("test", O_CREAT, S_IRWXU | S_IROTH | S_IXOTH | S_ISUID);

3.4.2 create函数

1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <fcntl.h>
4 int create(const char *pathname, mode_t mode);

参数:

pathname:要打开或创建的文件路径

mode:新建文件的访问权限,同umask一起决定文件的最终权限(mode & umask),其中umask代表了文件在创建时需要去掉的一些存取权限。umask可以通过系统调用umask()来改变:

/* 将umask设置为新的newmask,返回旧的umask */
int umask(int newmask);

返回:若成功为只写打开的文件描述符,若出错为-1

此函数等效于:

open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode);

create的一个不足之处是它以只写方式打开所创建的文件。

3.4.3 close函数

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

返回:若成功返回0,若出错返回-1

功能:关闭一个打开的文件。

3.4.4 read函数

1 #include <unistd.h>
2 ssize_t read(int fd, void *buf, size_t count);

返回:实际读到的字节数,若已到文件尾为0,若出错为-1

功能:从文件中读取数据。

参数:fd:读取文件的文件描述符指针

buf:存放读取数据的缓存

count:要求读取一次数据的字节数

有多种情况可使实际读到的字节数小于要求读字节数:

  1. 读普通文件时,当读到要求字节数之前已达到了文件尾端;
  2. 当从终端设备读时,通常一次最多读一行;
  3. 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数;
  4. 某些面向记录的设备,例如磁带,一次最多返回一个记录;
  5. 进程由于信号造成中断。

读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读得的字节数。

3.4.5 write函数

1 #include <unistd.h>
2 ssize_t write(int fd, const void *buf, size_t count);

返回:若成功返回实际写的字节数,若出错返回-1

功能:向打开的文件中写数据

参数:fd:写入文件的文件描述符;

buf:存放待写数据的缓存;

count:要求写入一次数据的字节数;

若返回值与count的值不同,表示出错。

write出错的一个常见原因:磁盘已写满或者超过了对一个给定进程的文件长度限制。对于普通该文件,写操作从文件的当前位移量处开始。如果在打开该文件时,指定了O_APPEDN选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处,在一次成功写之后,该文件位移量增加实际写的字节数。

3.4.6 lseek函数

1 #include <sys/types.h>
2 #include <unistd.h>
3 off_t lseek(int fd, off_t offset, int whence);

返回:若成功则返回新的文件偏移量(绝对偏移量:相对起始位置),若出错返回-1

功能:定位一个已打开的文件,随机指定位置进行读写。

参数:fd:已打开文件的文件描述符

offset:位移量

whence:定位的位置,基准点。SEEK_SET:将该文件的位移量设置为距文件开始处offset个字节;SEEK_CUR:将文件的位移量设置为其当前值加offset,offset可正可负;SEEK_END:将该文件的位移量设置为文件长度加offset,offset可正可负,正数表示一个空洞文件。

lseek也可用来确定所涉及的文件是否可以设置位移量,如果文件描述符引用的是一个管道或FIFO,则lseek返回-1,并将errno设置为EPIPE。

每个打开的文件都有一个与其相关联的"当前文件偏移量"。它是一个非负整数,用以度量从文件开始处计算的字节数。通常读、写操作都从文件偏移量处开始,并使偏移量加所读或写的字节数。按系统默认,当打开一个文件时,除非指定O_APPEND选择项,否则该位移量被设置为0。

注:文件读写buffer的大小一般设置为磁盘块的大小,磁盘块的大小可以通过命令查看:

df -T查看分区

$ df -T
Filesystem     Type     1K-blocks     Used Available Use% Mounted on
/dev/sda3      xfs       39517336 36730740   2786596  93% /
devtmpfs       devtmpfs    918820        0    918820   0% /dev
tmpfs          tmpfs       933512        0    933512   0% /dev/shm
tmpfs          tmpfs       933512     9228    924284   1% /run
tmpfs          tmpfs       933512        0    933512   0% /sys/fs/cgroup
/dev/sda1      xfs         303780   160328    143452  53% /boot
tmpfs          tmpfs       186704       12    186692   1% /run/user/42
tmpfs          tmpfs       186704        0    186704   0% /run/user/1000

sudo tune2fs -l /dev/sda1其中有个信息是:Block size: 4096

3.4.7 dup和dup2函数

1 #include <unistd.h>
2 int dup(int oldfd);
3 int dup2(int oldfd, int newfd);

返回:成功返回新文件描述符,出错返回-1

功能:文件描述符的复制

参数:oldfd:原先的文件描述符;newfd:新的文件描述符

说明:由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。

用dup2则可以用newfd参数指定新描述符的数值,如果newfd已经打开,则先将其关闭,若oldfd等于newfd,则dup2返回newfd,而不关闭它。

在进程间通信时可用来改变进程的标准输入和标准输出设备。

cat < 输入重定向文件 > 输出重定向文件 >> 追加输出重定向文件

dup2复制过程:

img

3.4..8 fcntl函数

1 #include <unistd.h>
2 #include <fcntl.h>
3 int fcntl(int fd, int cmd);
4 int fcntl(int fd, int cmd, long arg);
5 int fcntl(int fd, int cmd, struct float *lock);

返回:若成功则依赖于cmd,若出错返回-1

功能:可以改变已经打开的文件性质

常见的功能:复制一个现存的描述符,新文件描述符作为函数值返回(cmd=F_DUPFD);

获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD);

获取/设置文件状态标志(cmd=F_GETFL或F_SETFL);

获取/设置文件锁(cmd=F_SETLK、cmd=F_GETLK、F_SETLKW)第三个参数为struct flock结构体。

cmd的常见取值:

F_DUPFD:复制文件描述符,新的文件描述符作为函数返回值返回

F_GETFD/F_SETFS:获取/设置文件描述符,通过第三个参数设置(arg)

F_GETFL/F_SETFL:获取/设置文件状态标志,通过第三个参数设置(arg),可以更改的几个标志是:O_APPEDN、O_NONBLOCK、O_SYNC、O_ASYNC(O_RDONLY、O_WRONLY和O_RDWR不适用)

给文件上锁:cmd:F_SETLK(非阻塞式)、F_GETLK和F_SETLKW(阻塞式)

1 struct flock{
2   short l_type;
3   off_t l_start;
4   short l_whence;
5   off_t l_len;
6   pid_t l_pid;
7 };

l_type:锁类型,F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)。

l_start、l_whence:要加锁或解锁的区域的起始地址,由l_start和l_whence两者决定,l_start是相对位移量,l_whence则决定相对位移量的起点。

l_len:表示区域的长度。

3.4.10 ioctl函数

1 #include <unistd.h>
2 #include <sys/ioctl.h>
3 int ioctl(int fd, int request, …);

返回:若成功则为其他值,出错返回-1

说明:I/O操作的杂物箱。不能用其他函数表示的I/O操作通常都能用ioctl表示,终端I/O是ioctl的最大使用方面,主要用于设备的I/O控制。

4 测试案例

代码文档组织结构:

img

bin文件存放可执行文件,include存放头文件,obj存放.o文件,src存放源文件。

4.1 案例一:文件拷贝

简介:通过代码实现拷贝一个文件到另一个文件,涉及到open、read、write等系统IO调用。

src目录下添加io.c文件 cp.c文件,include目录存放io.h文件:

io.c代码:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include "io.h"

#define BUFFER_LEN 1024

void copy(int fdin, int fdout)
{
    char buffer[BUFFER_LEN];
    ssize_t size;
    while ((size = read(fdin, buffer, BUFFER_LEN)) > 0) {
           if (write(fdout, buffer, size) != size) {
              fprintf(stderr, "write error: %s\n", strerror(errno));
              exit(1);
           }
    }
    if (size < 0) {
       fprintf(stderr, "read error: %s\n", strerror(errno));
       exit(1);
    }
}

cp.c代码:

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include "io.h"

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s srcfile, destfile\n", argv[0]);
        exit(1);
    }

    int fdin, fdout;
    fdin = open(argv[1], O_RDONLY);
    if (fdin < 0) {
        fprintf(stderr, "open error: %s\n", strerror(errno));
        exit(1);
     } else {
        printf("open file: %d\n", fdin);
     }

    //打开一个待写入的文件,文件不存在就创建一个文件
    fdout = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0777);
    if (fdout < 0) {
        fprintf(stderr, "open error: %s\n", strerror(errno));
        exit(1);
    } else {
        printf("open file: %d\n", fdout);
    }

    //拷贝
    copy(fdin, fdout);

    close(fdin);
    close(fdout);


    return 0;
}

io.h文件:

#ifndef __IO_H__
#define __IO_H__

extern void copy(int fd1, int fd2);

#endif

编译:

[grace@laptop-untitvop io]$ gcc -o obj/io.o -Iinclude -c src/io.c  
[grace@laptop-untitvop io]$ gcc -o bin/cp -Iinclude obj/io.o src/cp.c

运行测试:

[grace@laptop-untitvop io]$ bin/cp /etc/passwd ./passwd

img

4.2 案例二:创建空洞文件

说明:创建一个空洞文件。

src目录添加一个hole_file.c文件:

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

char *buffer = "0123456789";

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "usage: %s [file]\n", argv[0]);
        exit(1);
    }

    int fd;
    fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0777);
    if (fd < 0) {
        perror("open error");
        exit(1);
    }

    ssize_t size = strlen(buffer) * sizeof(char);
    //将字符串写入空洞文件中
    if (write(fd, buffer, size) != size) {
        perror("write error");
        exit(1);
    }
    //定位到文件尾部10个字节处
    if (lseek(fd, 10L, SEEK_END) < 0) {
        perror("lseek error");
        exit(1);
    }
    //从文件尾部的10个字节处再写入字符串
    if (write(fd, buffer, size) != size) {
        perror("write error");
        exit(1);
    }

    close(fd);

    return 0;
}

编译:

[grace@laptop-untitvop io]$ gcc -o bin/hole_file src/hole_file.c 

运行测试:

img

4.3 案例三:实现自定义cat命令

说明:实现自定义cat命令,另外使用"+" 表示输入重定向,使用”-“表示输出重定向。

在src目录添加mcat.c文件:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include "io.h"

/* bin/mcat + iotek.txt (+为输入重定向)
 * bin/mcat - iotek.txt (-为输出重定向)
 */
int main(int argc, char *argv[])
{
    int fd_in, fd_out;
    int flag = 0;

    int i;
    for (i = 1; i < argc; i++) {
        if (!strcmp("+", argv[i])) {
            fd_in = open(argv[++i], O_RDONLY);
            if (fd_in < 0) {
                perror("open error");
                exit(1);
            }
            //将标准输入重定向到文件
            if (dup2(fd_in, STDIN_FILENO)) {
                perror("dup2 error");
                exit(1);
            }
            close(fd_in);
        } else if (!strcmp("-", argv[i])) {
            fd_out = open(argv[++i], O_WRONLY | O_CREAT | O_TRUNC, 0777);
            if (fd_out < 0) {
                perror("open error");
                exit(1);
            }
            //将标准输出重定向到文件
            if (dup2(fd_out, STDOUT_FILENO) != STDOUT_FILENO) {
                perror("dup2 error");
                exit(1);
            }
            close(fd_in);
        } else {
            flag = 1;
            fd_in = open(argv[i], O_RDONLY);
            if (fd_in < 0) {
                perror("open error");
                exit(1);
            }
            if (dup2(fd_in, STDIN_FILENO) != STDIN_FILENO) {
                perror("dup2 error");
                exit(1);
            }
            copy(STDIN_FILENO, STDOUT_FILENO);
            close(fd_in);
        }
    }

    if (flag == 0) {
        copy(STDIN_FILENO, STDOUT_FILENO);
    }

    return 0;
}

测试:

img

posted @ 2019-12-10 22:29  zhengcixi  阅读(813)  评论(0编辑  收藏  举报
回到顶部