嵌入式linux入门3-文件IO

文件描述符

每个打开的Linux文件都有一个对应的文件描述符,文件描述符为一个非负整数。我们可以通过调用open()函数获取文件描述符。

当shell开启一个进程时,此进程默认会继承三个文件描述符,称之为标准文件描述符,如下所示:

文件描述符 用途 POSIX名称 stdio流
0 标准输入 STDIN_FILENO stdin
1 标准输出 STDOUT_FILENO stdout
2 标准错误 STDERR_FILENO stderr

在程序中指代这些文件描述符时,可以直接使用数字,也可使用<unistd.h>中定义的POSIX名称**(推荐)**。但是stdio流对应的文件描述符并不是一成不变的,这只是它们的初始值,调用freopen()函数可能会改变stdio流对应的文件描述符数值,参考如下引用:

Although the variables stdin, stdout, and stderr initially refer to the process’s standard input , output, and error, thry can be changed to refer to any file by using the freopen() libraty function. As part of its operation, freopen() may change the file descriptor underlying the reopened stream. In other words, after an freopen() on stdout, for example, it is no longer safe to assume that the underlying file dexcriptor is still 1.

文件I/O函数

执行文件I/O的4个主要系统调用函数如下(语言和软件包通常会利用二次封装的I/O函数来间接调用它们,比如C库中的文件操作类函数:fopen(), fread(), fwrite(), fclose()):

int open(const char *pathname, int flags, mode_t mode);
// 打开pathname所指定的文件,并返回文件描述符,后续的函数使用文件描述符指代打开的文件,可以通过设置掩
// 码参数flags的方式在文件不存在时创建之, mode参数用于指定创建文件时文件的访问权限。
ssize_t read(int fd void *buf, size_t count);
// 从fd文件描述符所指代的文件中读取最多count字节的数据到缓冲buf,函数返回实际读取的字节数,如果读到文
// 件末尾,则返回0。
ssize_t write(int fd, const void *buf, size_t count);
// 从buf缓冲中读取count字节的数据并写入fd文件描述符所指代的文件中,函数返回实际写入文件的字节数。
int close(int fd);
//关闭文件描述符fd,此函数会释放文件描述符fd以及与之相关的内核资源。

如下是一个简单的cp命令的源代码,演示了上面四个函数的实际应用。

/*
 * file : main.c
 * 
 */

#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

#ifndef BUF_SIZE /* Allow "cc -D" to override definition */
#define BUF_SIZE 1024
#endif

int main(int argc, char *argv[])
{
    
    
    int inputFd, outputFd, openFlags;
    mode_t filePerms;
    ssize_t numRead;
    char buf[BUF_SIZE];
    
    if (argc != 3 || strcmp(argv[1], "--help") == 0)
    {
    
    
        printf("[usage]: %s old-file now-file\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    /* open input and output files */
    
    inputFd = open(argv[1], O_RDONLY);
    if (inputFd == -1)
    {
    
    
        printf("[error]: opening file %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }
    
    openFlags = O_CREAT | O_WRONLY | O_TRUNC; /* O_TRUNC clear file content */
    filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
        		S_IROTH | S_IWOTH; /* rw-rw-rw */
    outputFd = open(argv[2], openFlags, filePerms);
    if (outputFd == -1)
    {
    
    
        printf("[error]: opening file %s\n", argv[2]);
        exit(EXIT_FAILURE);
    }
    
    /* transfer data until we encounter end of input or an error */
    
    while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
    {
    
    
        if (write(outputFd, buf, numRead) != numRead)
        {
    
    
            printf("[error]: couldn't write whole buffer\n");
        	exit(EXIT_FAILURE);
        }
    }
    
    if (numRead == -1)
    {
    
    
        printf("[error]: read\n");
        exit(EXIT_FAILURE);
    }
    
    if (close(inputFd) == -1)
    {
    
    
        printf("[error]: close input\n");
        exit(EXIT_FAILURE);
    }
    
    if (close(outputFd) == -1)
    {
    
    
        printf("[error]: close output\n");
        exit(EXIT_FAILURE);
    }
    
    exit(EXIT_SUCCESS);
}

通用I/O

UNIX I/O模型的显著特点之一是其输入/输出的通用性概念,也就是我们常说的万物皆文件,使用上面所示的系统I/O调用函数可以对任何文件进行I/O操作,比如我们可以这样用上面的示例代码:

// 假设代码编译成可执行文件cp1,并且cp1位于当前目录下
./cp1 /dev/tty 1.txt // 从终端读取输入并写入文件1.txt
./cp1 1.txt /dev/tty // 将文件1.txt的内容打印到终端

其他常用函数

lseek()

off_t lseek(int fd, off_t offset, int whence);
// 将fd文件描述符的读写指针从whence所描述的位置为起点,移动offset个字节。
// whence可以为SEEK_SET(文件开头),SEEK_CUR(当前位置),SEEK_END(文件结尾后的第一个字节)
// 函数返回调整后文件读写指针所指位置相对文件开头的偏移值

//常用使用技巧

lseek(fd, 0, SEEK_SET);   		/* Start of file */
lseek(fd, 0, SEEK_END); 		/* Next byte after the end of the file */
lseek(fd, -1, SEEK_END); 		/* Last byte of file */
lseek(fd, -10, SEEK_CUR); 		/* Ten bytes prior to current location */
lseek(fd, 10000, SEEK_END);		/* 10001 bytes past last byte of file */

image-20220320211322226

此函数用于改变文件描述符中文件读写指针的偏移量。对于每个打开的文件,系统内核会记录其文件偏移量将其作为执行下一个write()或read()操作的文件起始位置。

lseek并不适用于所有类型的文件,lseek()不适用于管道、FIFO、socket或者终端。当用于上述文件时,调用会失败并将errno全局变量设置为ESPIPE。

如果程序的文件偏移量越过了文件结尾,然后执行I/O操作,read()函数会返回0表示文件结尾,而write()函数则可以在文件结尾后的任意位置写入数据,**从文件结尾后到新写入数据的这段空间称为文件空洞。**对于程序而言这段空间是一段填充了0的文件内容,而对于磁盘来说,这段空间是不占用任何磁盘空间的(所以会出现文件大小大于占用磁盘空间大小的情况)。

以下为一个示例程序演示了lseek协同read和write一起使用时的用法,《UNIX系统编程手册》里的例程用了很多自己写的库函数,我简单替换了它们,这样直接就可以运行了而不用去下载源码,当然也就不那么”健壮“。。。

/*
 * file : lseek_io.c
 */

// 该程序的第一个命令行参数为要打开的文件名称
// 余下的参数则指定了在文件上执行的输入/输出操作。每个表示操作的参数都以一个字母开头,
// 紧跟操作相关的值(中间不需要空格分割)。
// s<offset> : 从文件开始检索到offset字节位置
// r<length> : 在当前文件偏移量处读取length字节内容并以文本形式显示
// R<length> : 在当前文件偏移量处读取length字节内容并以十六进制形式显示
// w<str>    : 在当前文件偏移量处向文件写入str指定的字符串

#include <ctype.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

void usageErr(const char *format, ...)
{
    
    
    va_list argList;
    fflush(stdout);
    fprintf(stderr, "Usage: ");
    va_start(argList, format);
    vfprintf(stderr, format, argList);
    va_end(argList);

    fflush(stderr);
    exit(EXIT_FAILURE);
}

void errExit(const char *format, ...)
{
    
    
    va_list argList;
    fflush(stdout);
    fprintf(stderr, "Error: ");
    va_start(argList, format);
    vfprintf(stderr, format, argList);
    va_end(argList);

    fflush(stderr);
    exit(EXIT_FAILURE);
}

size_t getLong(char *str)
{
    
    
    long unsigned num;
    if (sscanf(str, "%lu", &num) > 0)
    {
    
    
        return (size_t)num;
    }
    else
    {
    
    
        return 0;
    }
}

int main(int argc, char *argv[])
{
    
    
    size_t len;
    off_t offset;
    int fd, ap, j;
    char *buf;
    ssize_t numRead, numWritten;

    if (argc < 3 || strcmp(argv[1], "--help") == 0)
    {
    
    
        usageErr("%s file {r<length>|R<length>|w<string>|s<offset>}...\n", argv[0]);
    }

    fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); /* rw-rw-rw- */
    if (fd == -1)
    {
    
    
        errExit("open");
    }

    for (ap = 2; ap < argc; ap++)
    {
    
    
        switch (argv[ap][0])
        {
    
    
        case 'r': /* Display bytes at current offset, as text */
        case 'R': /* Display bytes at current offset, as hex */
            len = getLong(&argv[ap][1]);
            buf = malloc(len);
            if (buf == NULL)
            {
    
    
                errExit("malloc");
            }

            numRead = read(fd, buf, len);
            if (numRead == -1)
            {
    
    
                errExit("read");
            }

            if (numRead == 0)
            {
    
    
                printf("%s: end-of-file\n", argv[ap]);
            }
            else
            {
    
    
                printf("%s :", argv[ap]);
                for (j = 0; j < numRead; j++)
                {
    
    
                    if (argv[ap][0] == 'r')
                    {
    
    
                        printf("%c", isprint((unsigned char)buf[j]) ? buf[j] : '?');
                    }
                    else
                    {
    
    
                        printf("%02x ", (unsigned int)buf[j]);
                    }
                }
                printf("\n");
            }

            free(buf);
            break;

        case 'w': /* write string at current offset */
            numWritten = write(fd, &argv[ap][1], strlen(&argv[ap][1]));
            if (numWritten == -1)
            {
    
    
                errExit("write");
            }
            printf("%s : wrote %ld bytes\n", argv[ap], (long) numWritten);
            break;

        case 's': /* Change file offset */
            offset = getLong(&argv[ap][1]);
            if (lseek(fd, offset, SEEK_SET) == -1)
            {
    
    
                errExit("lseek");
            }
            printf("%s: seek succeeded\n", argv[ap]);
            break;

        default:
            fprintf(stderr, "Argument must start with [rRws]: %s\n", argv[ap]);
        }
    }

    exit(EXIT_SUCCESS);
}

以下shell指令过程演示了上面示例程序的运行效果以及文件空洞现象。

water@LAPTOP-Q3TGG09O:~/lseek_io$ touch file
water@LAPTOP-Q3TGG09O:~/lseek_io$ ./lseek_io file s100000 wabc
s100000: seek succeeded
wabc : wrote 3 bytes
water@LAPTOP-Q3TGG09O:~/lseek_io$ ls -l file
-rw-r--r-- 1 water water 100003 Mar 20 22:48 file
water@LAPTOP-Q3TGG09O:~/lseek_io$ ./lseek_io file s10000 R5
s10000: seek succeeded
R5 :00 00 00 00 00

ioctl()

int ioctl(int fd, unsigned long request, ...);
// 用于执行通用I/O模型之外的文件操作,比如底层设备需要进行一些通过标准I/O函数无法进行的特殊的配置,
// 则可以通过这个接口进行
// fd为文件描述符,request为特定的操作请求,后面会跟一个或多个操作相关的参数,也可能没有

The fd argument is an open file descriptor for the device or file upon which the control operation specified by request is to be performed. Device-specific header files define constants that can be passed in the request argument. As indicated by the standard C ellipsis (…) notation, the third argument to ioctl(), which we label argp, can be of any type. The value of the request argument enables ioctl() to determine what type of value to expect in argp. Typically, argp is a
pointer to either an integer or a structure; in some cases, it is unused.

总结

对于普通的文件操作,一般我们先使用open()函数打开文件并获取文件描述符,之后使用read()/write()/lseek()函数进行相关的操作,最后使用close()关闭文件。

对于未纳入通用I/O模型的所有设备和文件操作,使用ioctl()函数进行。

猜你喜欢

转载自blog.csdn.net/lczdk/article/details/123624664
今日推荐