Linux I/O函数

pipe函数

pipe函数可用于创建一个管道,以实现进程间通信。

#include<unistd.h>

/* Create a one-way communication channel (pipe).
   If successful, two file descriptors are stored in PIPEDES;
   bytes written on PIPEDES[1] can be read from PIPEDES[0].
   Returns 0 if successful, -1 if not.  */
extern int pipe (int __pipedes[2]) __THROW __wur;

pipe函数的参数是-一个包含两个int 型整数的数组指针。该函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组。如果失败,则返回-1并设置errmo。

通过pipe函数创建的这两个文件描述符fd[0]和fd[]分别构成管道的两端,往fd[1]写人的数据可以从fd[0] 读出。并且,fd[0] 只能用于从管道读出数据,fd[1] 则只能用于往管道写入数据,而不能反过来使用。如果要实现双向的数据传输,就应该使用两个管道。默认情况下,这一对文件描述符都是阻塞的。此时如果我们用read系统调用来读取一个空的管道,则read将被阻塞,直到管道内有数据可读;如果我们用write系统调用来往一个满的管道中写人数据,则write亦将被阻塞,直到管道有足够多的空闲空间可用。但如果应用程序将fd[0]和fd[1]都设置为非阻塞的,则read和write会有不同的行为。关于阻塞和非阻塞的讨论。如果管道的写端文件描述符fd[1]的引用计数减少至0,即没有任何进程需要往管道中写人数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End Of File,EOF); 反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。

管道内部传输的数据是字节流,这和TCP字节流的概念相同。但二者又有细微的区别。应用层程序能往一个TCP连接中写人多少字节的数据,取决于对方的接收通告窗口的大小和本端的拥塞窗口的大小。而管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写人多少字节的数据。自Linux 2.6.11内核起,管道容量的大小默认是65536字节。我们可以使用fentl函数来修改管道容量。

此外,socket 的基础API中有-一个socketpair函数。它能够方便地创建双向管道。其定义如下:

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

/* Create two new sockets, of type TYPE in domain DOMAIN and using
   protocol PROTOCOL, which are connected to each other, and put file
   descriptors for them in FDS[0] and FDS[1].  If PROTOCOL is zero,
   one will be chosen automatically.  Returns 0 on success, -1 for errors.  */
extern int socketpair (int __domain, int __type, int __protocol,
		       int __fds[2]) __THROW;

socketpair前三个参数的含义与socket系统调用的三个参数完全相同,但domain只能使用UNIX本地域协议族AF_ _UNIX,因为我们仅能在本地使用这个双向管道。最后一个参数则和pipe系统调用的参数一样,只不过socketpair创建的这对文件描述符都是既可读又可写的。socketpair 成功时返回0,失败时返回-1并设置ermno。

dup函数和dup2函数

#include<unistd.h>

/* Duplicate FD, returning a new file descriptor on the same file.  */
extern int dup (int __fd) __THROW __wur;

/* Duplicate FD to FD2, closing FD2 and making it open on the same file.  */
extern int dup2 (int __fd, int __fd2) __THROW;

dup函数创建一个新的文件描述符,该新文件描述符和原有文件描述符file_descriptor 指向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小整数值。dup2 和dup类似,不过它将返回第一个不小于file_descriptor_two 的整数值。dup 和dup2系统调用失败时返回-1并设置errno。

readv函数和writev函数

readv函数将数据从文件描述符读到分散的内存块中,即分散读; writev 函数则将多块分散的内存数据一并写人文件描述符中,即集中写。它们的定义如下:

#include<sys/uio.h>

/* Read data from file descriptor FD, and put the result in the
   buffers described by IOVEC, which is a vector of COUNT 'struct iovec's.
   The buffers are filled in the order specified.
   Operates just like 'read' (see <unistd.h>) except that data are
   put in IOVEC instead of a contiguous buffer.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t readv (int __fd, const struct iovec *__iovec, int __count)
  __wur;

/* Write data pointed by the buffers described by IOVEC, which
   is a vector of COUNT 'struct iovec's, to file descriptor FD.
   The data is written in the order specified.
   Operates just like 'write' (see <unistd.h>) except that the data
   are taken from IOVEC instead of a contiguous buffer.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t writev (int __fd, const struct iovec *__iovec, int __count)
  __wur;

fd参数是被操作的目标文件描述符。vector 参数的类型是iovec结构数组。该结构体描述一块内存区。 count 参数是vector数组的长度,即有多少块内存数据需要从fd读出或写到fd。readv和writev在成功时返回读出1写入fd的字节数,失败则返回-1并设置ermno。它们相当于简化版的recvmsg和sendmsg函数。

sendfile

sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。sendfile 丽数的定义如下:

#include<sys/sendfile.h>

/* Send up to COUNT bytes from file associated with IN_FD starting at
   *OFFSET to descriptor OUT_FD.  Set *OFFSET to the IN_FD's file position
   following the read bytes.  If OFFSET is a null pointer, use the normal
   file position instead.  Return the number of written bytes, or -1 in
   case of error.  */
extern ssize_t sendfile (int __out_fd, int __in_fd, off_t *__offset,
			 size_t __count) __THROW;

in_ fd 参数是待读出内容的文件描述符,out_fd参数是待写入内容的文件描述符。offset参数指定从读人文件流的哪个位置开始读,如果为空,则使用读人文件流默认的起始位置。count参数指定在文件描述符in_fd 和out_fd 之间传输的字节数。sendfile 成功时返回传输的字节数,失败则返回-1并设置errmo。该函数的man手册明确指出,in_fd 必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;而out fd则必须是一个socket。由此可见,sendfile 几乎是专门为在网络上传输文件而设计的。

mmap函数和munmap函数

mmap函数用于申请一段内存空间。我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。munmap 函数则释放由mmap创建的这段内存空间。它们的定义如下:

#include<sys/mman.h>

/* Map addresses starting near ADDR and extending for LEN bytes.  from
   OFFSET into the file FD describes according to PROT and FLAGS.  If ADDR
   is nonzero, it is the desired mapping address.  If the MAP_FIXED bit is
   set in FLAGS, the mapping will be at ADDR exactly (which must be
   page-aligned); otherwise the system chooses a convenient nearby address.
   The return value is the actual mapping address chosen or MAP_FAILED
   for errors (in which case `errno' is set).  A successful `mmap' call
   deallocates any previous mapping for the affected region.  */

extern void *mmap (void *__addr, size_t __len, int __prot,
		   int __flags, int __fd, __off_t __offset) __THROW;
		   
/* Deallocate any mapping for the region starting at ADDR and extending LEN
   bytes.  Returns 0 if successful, -1 for errors (and sets errno).  */
extern int munmap (void *__addr, size_t __len) __THROW;
  • start参数允许用户使用某个特定的地址作为这段内存的起始地址。如果它被设置成NULL,则系统自动分配一个地址。length 参数指定内存段的长度。

  • prot 参数用来设置内存段的访问权限。它可以取以下几个值的按位或:

    • PROT_READ,内存段可读。
    • PROT_WRITE,内存段可写。
    • PROT_EXEC,内存段可执行。
    • PROT_NONE,内存段不能被访问。
  • flags参数控制内存段内容被修改后程序的行为。它可以被设置为下表中的某些值(这里仅列出了常用的值)的按位或(其中MAP_ SHARED和MAP_PRIVATE 是互斥的,不能同时指定)。
    Linux I/O函数

  • fd参数是被映射文件对应的文件描述符。它一般通过open系统调用获得。

  • offset 参数设置从文件的何处开始映射(对于不需要读入整个文件的情况)。

mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED((void*)-1)并设置errno。munmap函数成功时返回0,失败则返回-1并设置ermo。

splice函数

splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。splice 函数的定义如下:

#include <fcntl.h>

ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);

fd_in参数是待输人数据的文件描述符。如果fd_in是一个管道文件描述符,那么off_in参数必须被设置为NULL。如果fd_in 不是一个管道文件描述符(比如socket),那么off_in表示从输人数据流的何处开始读取数据。此时,若off_in 被设置为NULL,则表示从输人数据流的当前偏移位置读人;若off_in 不为NULL,则它将指出具体的偏移位置。fd_out/off_out参数的含义与fd_in/off_in相同,不过用于输出数据流。len 参数指定移动数据的长度;flags参数则控制数据如何移动,它可以被设置为下表中的某些值的按位或。
Linux I/O函数
使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量。它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据(fd_in是管道文件描述符)而该管道没有被写人任何数据时。splice 丽数失败时返回-1并设置erro。常见的ermno如下表所示。
Linux I/O函数

tee函数

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。tee 函数的原型如下:

#include<fcntl.h>

ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

该函数的参数的含义与splice相同(但fd_in 和fd_out必须都是管道文件描述符)。tee函数成功时返回在两个文件描述符之间复制的数据数量(字节数)。返回0表示没有复制任何数据。tee 失败时返回-1并设置errno。

fcntl函数

fcntl函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操作。另外一个常见的控制文件描述符属性和行为的系统调用是ioctl,而且ioctl比fentI能够执行更多的控制。但是,对于控制文件描述符常用的属性和行为,fentl函数是由POSIX规范指定的首选方法。fentl 函数的定义如下:

#include<fcntl.h>

/* Do the file control operation described by CMD on FD.
   The remaining arguments are interpreted depending on CMD.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int fcntl (int __fd, int __cmd, ...);

fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操作。根据操作类型的不同,该函数可能还需要第三个可选参数arg。fentl 函数支持的常用操作及其参数如下表所示。
Linux I/O函数
Linux I/O函数
fentl丽数成功时的返回值如表最后一列所示,失败则返回-1并设置ermo。

在网络编程中,fcntl 函数通常用来将-一个文件描述符设置为非阻塞的,如代码所示:

int setnonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);		/*获取文件描述符旧的状态标志*/
    int new_option = old_option | O_NONBLOCK;   /* 设置非阻塞标志*/
    fcnt1(fd, F_SETFL, new_option);				/*返回文件描述符旧的状态标志,以便日后恢复该状态标志*/
    return old_option;
}

此外,SIGIO和SIURG这两个信号与其他Linux信号不同,它们必须与某个文件描述符相关联方可使用:当被关联的文件描述符可读或可写时,系统将触发SIGIO信号;当被关联的文件描述符(而且必须是一个socket)上有带外数据可读时,系统将触发SIGURG信号。将信号和文件描述符关联的方法,就是使用fcntl 函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。使用SIGIO时,还需要利用fcntl设置其O_ASYNC 标志(异步I/0标志,不过SIGIO信号模型并非真正意义上的异步IO模型)。

上一篇:彻底弄懂 Linux 下的文件描述符(fd)


下一篇:操作系统实验——进程通信