高级 IO 访问

更新时间:
2024-03-14
下载文档

高级 IO 访问

本节介绍高级 IO 的使用方法。

分散聚集操作

#include <sys/uio.h>
ssize_t  readv(int  iFd,  struct iovec *piovec, int iIovcnt);
ssize_t  writev(int  iFd, const struct iovec *piovec,int iIovcnt);

函数 readv 原型分析:

  • 此函数成功返回读取的字节数,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 输出参数 piovec 是分散缓冲区数组指针。
  • 参数 iIovcnt 是缓冲区个数。

函数 writev 原型分析:

  • 此函数成功返回写的字节数,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 piovec 是聚集缓冲区(要发送的数据)数组指针。
  • 参数 iIovcnt 是缓冲区个数。

readv 函数和 writev 函数用于在一次函数调用中读、写多个非连续缓冲区,有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。

这两个函数的第二个参数是指向 iovec 结构数组的指针:

struct iovec {
    PVOID    iov_base;                         /*  基地址                */
    size_t   iov_len;                          /*  长度                  */
};

结构中成员 iov_base 指向某一项缓冲区的首地址,成员 iov_len 是该缓冲区的长度。此两个函数的参数和 iovec 结构的关系如下图所示。

readv 函数从缓冲区读出的顺序是 piovec[0]、piovec[1]直到 piovec[iIovcnt-1],成功返回读到的总字节数,writev 函数写入的顺序和 readv 函数顺序相同,成功返回写入的总字节数。

下面程序展示了 readv 函数和 writev 函数的使用,程序定义了两个 iovec 元素,两个不同长度的缓冲区,调用 readv 函数从文件 a.test 中读取数据,然后调用 writev 函数将数据写到文件 b.test 中。

#include <stdio.h>
#include <sys/uio.h>

int main (int argc, char *argv[])
{
    int               fd;
    char              buf1[15], buf2[34];
    struct iovec      iov[2];
    ssize_t           ret;

    iov[0].iov_base = buf1;
    iov[0].iov_len  = sizeof(buf1);
    iov[1].iov_base = buf2;
    iov[1].iov_len  = sizeof(buf2);

    fd = open("a.test", O_RDONLY);
    if (fd < 0) {
        fprintf(stderr, "open file: %s failed.", "a.test");
        return  (-1);
    }

    ret = readv(fd, iov, 2);
    if (ret < 0) {
        fprintf(stderr, "readv error.\n");
        close(fd);
        return  (-1);
    }

    fprintf(stdout, "readv read bytes %ld\n", ret);
    close(fd);

    fd = open("b.test", O_WRONLY);
    if (fd < 0) {
        fprintf(stderr, "open file: %s failed.\n", "b.test");
        return  (-1);
    }

    ret = writev(fd, iov, 2);
    if (ret < 0) {
        fprintf(stderr, "writev error.\n");
        close(fd);
        return  (-1);
    }

    fprintf(stdout, "writev write bytes %ld\n", ret);
    close(fd);
    return  (0);
}

在 SylixOS Shell 下运行程序,从程序的运行结果可以看出,readv 函数会依次读两个缓冲区,而 writev 函数会依次将两个缓冲区中的数据写到指定的文件中。

# cat a.test
This is a sylixos test readv and writev example.
# cat b.test
This is a sylixos test readv and writev example.
# ./Functions_readv_writev
readv read bytes 49
writev write bytes 49

非阻塞 I/O

有一些“低速”系统函数可能会使进程永远阻塞,例如某些进程间通信函数、某些 ioctl 操作等。

非阻塞 I/O 使 open、read 和 write 这些操作不会永远阻塞(需要设备驱动程序提供支持)。如果这些操作不能完成,则调用立即出错返回,表示该操作如果继续执行将阻塞。

有下面的两种方法可以获得一个非阻塞的 I/O:

  • 调用 open 函数获得一个文件描述符,可以指定 O_NONBLOCK 标志。
  • 如果文件已经打开,则可以调用 ioctl 指定 FIONBIO 命令,或者调用 fcntl 指定 F_SETFL 选项。

多路 I/O 复用

多路 I/O 复用技术,需要先构造一张我们感兴趣的文件描述符列表,然后调用一个函数,直到这些描述符中的一个或多个已准备好进行 I/O 时,该函数才返回。

select、pselect、poll、ppoll 这 4 个函数可以实现多路 I/O 复用功能,从这些函数返回时,进程或者线程会被告知哪些文件描述符已准备好进行 I/O 操作。

下面详细介绍 select 函数、pselect 函数、poll 函数和 ppoll 函数的相关细节。

select 函数组

#include <sys/select.h>
int  select(int              iWidth,
            fd_set          *pfdsetRead,
            fd_set          *pfdsetWrite,
            fd_set          *pfdsetExcept,
            struct timeval  *ptmvalTO);
int  pselect(int                     iWidth, 
             fd_set                 *pfdsetRead,
             fd_set                 *pfdsetWrite,
             fd_set                 *pfdsetExcept,
             const struct timespec  *ptmspecTO,
             const sigset_t         *sigsetMask);

函数 select 原型分析:

  • 此函数成功返回等到的描述符数量,超时返回 0,失败返回-1 并设置错误号。
  • 参数 iWidth 是文件描述符列表中最大描述符加 1。
  • 参数 pfdsetRead 是读描述符集。
  • 参数 pfdsetWrite 是写描述符集。
  • 参数 pfdsetExcept 是异常描述符集。
  • 参数 ptmvalTo 是等待超时时间。

函数 pselect 原型分析:

  • 此函数成功返回等到的描述符数量,超时返回 0,失败返回-1 并设置错误号。
  • 参数 iWidth 是文件描述符列表中最大描述符加 1。
  • 参数 pfdsetRead 是读描述符集 。
  • 参数 pfdsetWrite 是写描述符集 。
  • 参数 pfdsetExcept 是异常描述符集 。
  • 参数 ptmspecTo 是等待超时时间。
  • 参数 sigsetMask 是等待时阻塞的信号。

我们来看 select 函数的参数,从传递给内核的参数可以看出下面这几点:

  • 告诉内核我们所关心的文件描述符。
  • 对于每个文件描述符我们所关心的条件(读、写、异常)。
  • 愿意等待多长时间(可以永远等待、可以不等待、可以等待指定的时间)。

当 select 返回时,我们可以得知多少文件描述符就绪了,以及哪些文件描述符就绪了,使用这些就绪的文件描述符可以进行 read、write 等操作。

参数 ptmvalTo 可以分 3 种情况:

  • 当 ptmvalTo == NULL 时,表示永远等待,。
  • 当 ptmvalTo->tv_sec == 0 && ptmvalTo->tv_usec == 0 时,表示不等待,。
  • 当 ptmvalTo->tv_sec != 0 || ptmvalTo->tv_usec != 0 时,表示等待指定的秒和微秒数。

参数 pfdsetRead、pfdsetWrite 和 pfdsetExcept 是指向文件描述符集的指针,每个文件描述符集存储在一个 fd_set 数据类型的变量中,这个类型,不同的系统可能有不同的实现,这里我们可以认为它是一个很大的字节数组,每一个文件描述符占据其中一位。在 SylixOS 中,对于 fd_set 类型的变量提供了下面一组宏进行操作:

宏名说明
FD_SET(n, p)将文件描述符 n 设置到文件描述符集 p 中
FD_CLR(n, p)将文件描述符 n 从文件描述符集 p 中清除
FD_ISSET(n, p)判断文件描述符 n 是否属于文件描述符集 p
FD_ZERO(p)将文件描述符集 p 清空

在声明一个 fd_set 类型的文件描述符集后,首先需要使用 FD_ZERO 清除该文件描述符集,然后使用 FD_SET 将我们关心的文件描述符放到该集中,当 select 成功返回后,使用 FD_ISSET 来判断是否是我们关心的文件描述符。

select 函数有 3 种可能的返回值:

  • 返回值-1 表示错误,例如,在所有指定的文件描述符都没有准备好时捕捉到一个信号,将返回-1。
  • 返回值 0 表示没有文件描述符准备好,因为在指定的时间内,没有文件描述符准备好,也即调用超时。
  • 返回值是一个大于 0 的整数,该值是 3 个文件描述符集中所有准备好的文件描述符数之和。

select 函数返回准备好的文件描述符和,这里“准备好”具有下面的意思:

  • 对于读集中的一个文件描述符 read 操作不会阻塞。
  • 对于写集中的一个文件描述符 write 操作不会阻塞。
  • 对于异常集中的一个文件描述符有一个未决异常条件。

需要注意的是,如果在一个文件描述符上碰到了文件尾端,则 select 函数会认为该文件描述符可读,然后调用 read 函数将返回 0。

在上面的定义中我们看到 pselect 函数除了最后两个参数和 select 函数不同外,其他参数是相同的,下面我们来介绍一下这两个不同的参数。

select 函数超时值类型是 struct timeval 而 pselect 函数超时值类型是 struct timespec(见时间管理章节),timespec 结构以秒和纳秒表示超时值,也就是说 pselect 函数提供了比 select 函数更精准的超时时间。

pselect 函数可使用信号屏蔽字。如 sigmask 为 NULL, pselect 函数的运行状况和 select 函数相同。否则,sigmask 指向一信号屏蔽字,在调用 pselect 函数时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。

下面程序展示了 select 函数的使用,程序等待标准输入(STDIN_FILENO)描述符可读,如果在超时时间内 select 返回,则读取标准输入,并打印读到的字符。

#include <stdio.h>
#include <sys/select.h>

int main (int argc, char *argv[])
{
    fd_set          fdset;
    int             ret;
    struct timeval  timeout;
    char            ch;

    timeout.tv_sec  = 10;
    timeout.tv_usec = 0;

    for (;;) {
        FD_ZERO(&fdset);
        FD_SET(STDIN_FILENO, &fdset);
        ret = select(STDIN_FILENO + 1, &fdset, NULL, NULL, &timeout);
        if (ret <= 0) {
            break;
        } else if (FD_ISSET(STDIN_FILENO, &fdset)){
            read(STDIN_FILENO, &ch, 1);
            if (ch == '\n') {
                continue;
            }
            fprintf(stdout, "input char: %c\n", ch);
            if (ch == 'q') {
                break;
            }
        }
    }
    return  (0);
}

在 SylixOS Shell 下运行程序,并输入字符,可以看到相应的打印。

# ./Function_select
h
input char: h

poll 函数组

poll 函数功能类似于 select 函数,但是函数接口有所不同。

#include <poll.h>

int  poll(struct pollfd fds[], nfds_t nfds, int timeout);
int  ppoll(struct pollfd           fds[], 
           nfds_t                  nfds, 
           const struct timespec  *timeout_ts,
           const sigset_t         *sigmask);

函数 poll 原型分析:

  • 此函数成功返回等到的描述符数量,超时返回 0,失败返回-1 并设置错误号。
  • 参数 fds 是 poll 文件描述符数组。
  • 参数 nfds 是 fds 数组的元素个数。
  • 参数 timeout 是超时值。

函数 ppoll 原型分析:

  • 此函数成功返回等到的描述符数量,超时返回 0,失败返回-1 并设置错误号。
  • 参数 fds 是 ppoll 文件描述符数组。
  • 参数 nfds 是 fds 数组的元素个数。
  • 参数 timeout_ts 是超时值。
  • 参数 sigmask 是等待时阻塞的信号。

与 select 函数不同,poll 函数不是为每个条件构造一个文件描述符集,而是构造一个 pollfd 结构的数组,每个数组元素指定一个文件描述符编号以及我们对该文件描述符感兴趣的条件。

struct pollfd {
    int         fd;                 /*  file descriptor being polled */
    short int   events;             /*  the input event flags        */
    short int   revents;            /*  the output event flags       */
};

调用 poll 函数时,应将每个 fds 元素中的 events 设置成下表中某个或某些值,通过这些值告诉内核我们关心的是每个文件描述符的哪些事件。返回时,revents 成员由内核设置,用于说明每个文件描述符发生了哪些事件。

表从上到下 3 行分别代表了可读、可写和异常。

宏名说明
POLLIN
POLLRDNORM
POLLRDBAND
POLLPRI
可以不阻塞地读高优先级以外的数据(等效于 POLLRDNORM 或 POLLRDBAND)
可以不阻塞读普通数据
可以不阻塞读优先级数据
可以不阻塞读高优先级数据
POLLOUT
POLLWRNORM
POLLWRBAND
可以不阻塞写普通数据
和 POLLOUT 相同
可以不阻塞写优先级数据
POLLERR
POLLHUP
已出错
已挂断

poll 函数的超时等待参数和 select 函数类似,也分 3 种情况。我们需要注意,poll 函数和 select 函数不会因为一个文件描述符是否阻塞而影响其本身的阻塞情况。

ppoll 函数的行为类似于 poll,不同的是,ppoll 函数可以指定信号屏蔽字。

文件记录锁

当一个进程正在读取或者修改文件的某个部分时,使用文件记录锁可以阻止其他进程修改同一文件的相同区域,它可以用来锁定文件的某个区域或者整个文件,SylixOS 支持多种文件记录锁 API。

之前我们说过 SylixOS 支持多种设备驱动模型,但是目前只有 NEW_1 型设备驱动支持文件记录锁功能,此类驱动文件节点类似于 UNIX 系统的 vnode。

下面我们首先介绍功能灵活的 fcntl 锁,函数的原型可参见“文件I/O”节 fcntl 部分。在之前介绍 fcntl 功能时,我们提到 fcntl 函数包含了操作文件记录锁的功能,此功能包含了 3 个命令:F_GETLK、F_SETLK 和 F_SETLKW,fcntl 记录锁的第 3 个参数是一个 flock 结构体指针。

struct flock {
    short    l_type;                /* F_RDLCK, F_WRLCK, or F_UNLCK         */
    short    l_whence;              /* flag to choose starting offset       */
    off_t    l_start;               /* relative offset, in bytes            */
    off_t    l_len;                 /* length, in bytes; 0 means            */
                                    /* lock to EOF                          */
    pid_t    l_pid;                 /* returned with F_GETLK                */
    ……
};

下面是 struct flock 结构成员含义:

  • l_type 是锁类型:F_RDLOCK(共享读锁)、F_WRLOCK(独占写锁)和 F_UNLCK(解锁)。
  • l_whence 值,如下表所示的值。
  • l_start 是相对 l_whence 偏移开始位置(注意不可以从文件开始的之前部分锁起)。
  • l_len 是锁定区域长度,如果为 0,则锁定文件尾(EOF),如果向文件中追加数据,也将被锁。
  • l_pid 是阻止当前进程加锁的进程 ID(由命令 F_GETLK 返回)。

上面提到共享读锁和独占写锁,基本规则是:任意多个进程在一个给定字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程有一把独占的写锁。进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上有一把写锁,则不能再加任何锁。

上面的规则适用于不同进程提出的锁请求,并不适用于单个进程提出的锁请求。也就是说,如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一个区间再加一把锁,那么也是可以的,这个时候新锁将替换已有锁。因此,如果一个进程将某个文件加了一把写锁后,又企图给文件加一把读锁,那么将会成功执行,原来的写锁会被替换为读锁,我们通过如下程序来验证这一观点。另外,加读锁时,该文件描述符必须是读打开的,加写锁时,该文件描述符必须是写打开的。

iWhence 值oftOffset 说明
SEEK_SET将文件的偏移量设置为距文件开始处 oftOffset 个字节
SEEK_CUR将文件的偏移量设置为当前值加 oftOffset 个字节,oftOffset 可为负
SEEK_END将文件的偏移量设置为文件长度加 oftOffset 个字节,oftOffset 可为负
#include <stdio.h>
#include <unistd.h>

int main (int argc, char *argv[])
{
    int           fd;
    struct flock  fl;
    short         lockt = F_WRLCK;

    fd = open("file", O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open file failed.\n");
        return -1;
    }

    fl.l_type   = lockt;
    fl.l_whence = SEEK_SET;
    fl.l_start  = 0;
    fl.l_len    = 0;

    if (fcntl(fd, F_SETLK, &fl) < 0) {
        perror("fcntl");
        fprintf(stderr, "add write lock failed.\n");
        close(fd);
        return -1;
    }
    fprintf(stdout, "add write lock success.\n");
    lockt         = F_RDLCK;
    fl.l_type     = lockt;
    fl.l_whence     = SEEK_SET;
    fl.l_start     = 0;
    fl.l_len     = 0;
    if (fcntl(fd, F_SETLK, &fl) < 0) {
        perror("fcntl");
        fprintf(stderr, "add read lock failed.\n");
        close(fd);
        return -1;
    }
    fprintf(stdout, "add read lock success.\n");
    return 0;
}
# ./Single_Lock_Process
add write lock success.
add read lock success.

SylixOS 中支持传统的 BSD 函数 flock 来锁定整个文件,该函数是一个旧式的 API。

#include <sys/file.h>
int  flock(int iFd, int iOperation);

函数 flock 原型分析:

  • 此函数成功返回 0,失败返回-1 并设置错误号。
  • 参数 iFd 是文件描述符。
  • 参数 iOperation 是锁类型(如下表所示)。

调用 flock 函数可以锁定一个打开的文件,该函数支持的锁类型如下表所示。进程使用 flock 函数尝试锁文件时,如果文件已经被其他进程锁定,进程会被阻塞直到锁被释放,或者在调用 flock 函数的时候,采用 LOCK_NB 参数,在尝试锁住该文件时,发现已经被其他进程锁住,会返回错误。

flock 函数可以调用带 LOCK_UN 参数来释放文件锁,也可以通过关闭 fd 的方式来释放文件锁,这意味着 flock 锁会随着进程的退出而被释放。

flock 锁类型说明
LOCK_SH共享锁,多个进程可以使用一把锁,常被用作读锁
LOCK_EX排他锁,同时只允许一个进程占用,常被用作写锁
LOCK_NB实现非阻塞,默认为阻塞
LOCK_UN解锁

SylixOS 支持 lockf 函数,lockf 函数是一种 POSIX 锁,可以看成是 fcntl 接口的封装。

#include <unistd.h>
int  lockf(int iFd, int iCmd, off_t oftLen);

函数 lockf 原型分析:

  • 此函数成功返回 0,失败返回-1。
  • 参数 iFd 是文件描述符。
  • 参数 iCmd 是锁命令。
  • 参数 oftLen 是锁定的资源长度,从当前偏移量开始。

调用 lockf 函数表现的行为基本和 fcntl 函数相同,lockf 函数传入的是文件描述符,此函数支持的锁命令如下表所示。

lockf 锁命令说明
F_ULOCK解锁
F_LOCK阻塞的方式请求排他锁,也即写锁
F_TLOCK非阻塞的方式请求排他锁,也即写锁
F_TEST获得指定文件区锁状态,测试是否加锁

文件内存映射

文件内存映射能将一个磁盘文件映射到内存空间中的一块内存区域上,当从缓冲区中取数据时,就相当于读文件中的相应字节。相应地,将数据存入缓冲区时,相应字节就自动写入文件。这样就可以在不使用 read 函数和 write 函数的情况下执行 I/O 操作。

为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个内存区域中,这是由 mmap 函数实现的,这些技术细节将在内存管理部分作介绍。

文档内容是否对您有所帮助?
有帮助
没帮助