十三章直接跳过了,内容上并不太多,直接看14章的内容,这一章的内容十分重要。
14.2 非阻塞式I/O
非阻塞I/O使我们可以发出open、read和write这样的I/O操作,并使这些操作不会永远阻塞。如果请求的I/O操作不能完成,则调用立即出错返回,表明此时数据还没有准备好,该操作如继续则仍将阻塞。
对于一个给定的描述符,有两种方法可以将其指定为非阻塞式I/O。
- 如果调用open获得描述符,则可指定O_NONBLOCK标志。
- 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK标志。
书中还给出了例子,我也试了一下,源码如下:
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
char buf[500000];
void set_fl(int fd , int flags);
void cli_fl(int fd , int flags);
int main()
{
int ntowrite,nwrite;
char *ptr;
ntowrite = read(STDIN_FILENO,buf,sizeof(buf));
fprintf(stderr,"read %d bytes\n",ntowrite);
set_fl(STDOUT_FILENO,O_NONBLOCK);
ptr = buf;
while(ntowrite>0){
errno = 0;
nwrite = write(STDOUT_FILENO,ptr,ntowrite);
fprintf(stderr,"nwrite = %d , errno = %d\n",nwrite,errno);
if(nwrite>0){
ptr += nwrite;
ntowrite -= nwrite;
}
}
cli_fl(STDOUT_FILENO,O_NONBLOCK);
return 0;
}
void set_fl(int fd , int flags)
{
int val;
if((val = fcntl(fd,F_GETFL,0))<0)
perror("fcntl F_GETFL error");
val |= flags;
if((val = fcntl(fd,F_SETFL,0))<0)
perror("fcntl F_GETFL error");
}
void cli_fl(int fd , int flags)
{
int val;
if((val = fcntl(fd,F_GETFL,0))<0)
perror("fcntl F_GETFL error");
val &= ~flags;
if((val = fcntl(fd,F_SETFL,0))<0)
perror("fcntl F_GETFL error");
}不过在我的机器上,并没有出现书中结果,可能是写入的数据量不够大。
14.3 记录锁
记录锁的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。记录锁锁定的是文件中的一个区域(也可能是整个文件)。
记录锁功能如下:
#include <fcntl.h>
extern int fcntl (int __fd, int __cmd, ...);其中“__cmd”可以是以下三个选项之一:F_GETLK、F_SETLK、F_SETLKW。
第三个参数是一个指向flock结构的指针。flock结构定义如下:
struct flock
{
short int l_type; /* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK. */
short int l_whence; /* Where `l_start' is relative to (like `lseek'). */
#ifndef __USE_FILE_OFFSET64
__off_t l_start; /* Offset where the lock begins. */
__off_t l_len; /* Size of the locked area; zero means until EOF. */
#else
__off64_t l_start; /* Offset where the lock begins. */
__off64_t l_len; /* Size of the locked area; zero means until EOF. */
#endif
__pid_t l_pid; /* Process holding the lock. */
};定义位于“/usr/include/i386-linux-gnu/bits/fcntl.h”。
对其中每个字段的功能进行一个简单的分析:
l_type是锁的类型,可以是以下三个类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁)。
l_whence表示起始位置。
l_start表示相对于起始位置的偏移量。
l_len表示加锁或解锁的数据长度。若l_len为0,则表示锁的范围可以扩展到最大可能偏移量。这意味着不管向该文件中追加写了多少数据,它们都可以处于锁的范围内。
l_pid表示操作的进程ID。
这里不同进程间使用锁的规则与读写锁的加锁方式相同。当同一进程间的锁规则有所不同:如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一个文件区间上再加一把锁。那么新锁将替换已有锁。
加读锁时,该描述符必须是读打开。加写锁时,该描述符必须是写打开。
让我们回过头来对fcntl命令字的意义进行详细研究。
F_GETLK:判断由flockptr所描述的锁是否会被另一把锁所阻塞。如果存在一把锁,它阻止创建由flockptr所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在一把锁,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。F_GETLK测试能否建立一把锁,然后用F_SETLK或F_SETLKW企图建立那把锁。但以上两个操作不是原子的
F_SETLK:设置由flockptr所描述的锁。如果试图获得一把锁,而兼容性规则阻止获得这把锁。那么fcntl会立即出错返回,此时errno设置为EAGAIN。
F_SETLKW:该命令是F_SETLK的阻塞版本。如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁而不能被授予,那么调用进程会被设置为休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒。
在设置或释放文件上的一把锁时,系统按要求组合或分裂相邻区。
书中还提到了一个死锁检测的例子,在这个例子中对于可能出现死锁的情况,记录锁能够避免。
关于记录锁的自动继承与释放共有三条规则:
- 锁与进程和文件相关联:当一个进程终止时,它所建立的锁全部释放。当文件描述符关闭时,则该文件描述符上由某个进程设置的锁也会释放。
- 由fork产生的子进程不继承父进程所设置的锁。
- 在执行exec后,新程序可以继承原执行程序的锁。
14.4 I/O多路转接
14.4.1 select函数与pselect函数
首先来看select函数的作用:允许进程指示内核等待多个事件中的一个发生,并只在有一个或多个时间发生,或定时器时间到时函数返回。函数原型如下:
extern int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);函数共包括三种返回值:
- 准备就绪的描述符数目。
- 若超时,返回0。
- 若出错,返回-1。
了解一下各个参数的意义,首先是最后一个参数“__timeout”。
- 若“__timeout”为空。永远等待。如果所指定的描述符中的一个已准备好或捕捉到一个信号则返回。若捕捉到一个信号,则返回-1,errno设置为EINTR。
- 若“__timeout”设置时间为0,则根本不等待,测试所有指定的描述符并返回。
- 若“__timeout”设置时间不为0,则等待相应的时间,当指定的描述符之一已准备好,或当指定时间值已经超时时立即返回。
中间3个参数“__readfds”、“__writefds”、“__exceptfds”是指向描述符的指针。fd_set的定义也比较简单,就是一个数组。
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
typedef long int __fd_mask;
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
对于fd_set进行操作要使用专门的函数:
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp) //开启描述符中的一位
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp) //清除描述符中的一位
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp) //测试描述符中的一位已经打开
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp) //将fd变量所有位置0
通过上述定义也可以发现,以上四种函数均是通过宏定义实现的。
从select函数返回后可以使用FD_ISSET判断描述符是否准备就绪。
再来看第一个参数“__nfds”,该参数是指“最大文件描述符编号值加1”。这一最大值是指三个描述符集中最大描述符编号值。
还有一个类似的函数pselect:
extern int pselect (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
const struct timespec *__restrict __timeout,
const __sigset_t *__restrict __sigmask);pselect在功能上与select的一点不同之处在于,pselect可以指定一个信号屏蔽字,在调用就可以屏蔽相应的信号,返回时,这些被屏蔽的信号则会恢复。
14.4.2 poll函数
poll函数与select函数在功能上基本相同,只是在接口上存在一定的差异,函数原型如下:
#include <poll.h>
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);其中struct pollfd定义如下:
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};其中fd是我们关心的文件描述符。events是我们关心的事件。返回时由内核设置revents代表实际发生的事件。
最后一个参数与select的最后一个参数类似,代表系统调用等待的毫秒数。
14.5 异步I/O
14.5.3 POSIX异步I/O
首先来看异步I/O控制块,定义位于/usr/include/aio.h中,详细分析请见注释
struct aiocb
{
int aio_fildes; /* File desriptor. */ //用于读或写的文件描述符
int aio_lio_opcode; /* Operation to be performed. */
int aio_reqprio; /* Request priority offset. */
volatile void *aio_buf; /* Location of buffer. */ //读写缓冲区
size_t aio_nbytes; /* Length of transfer. */ //读或写的字节数
struct sigevent aio_sigevent; /* Signal number and value. */ //io事件完成后,如何通知应用程序
/* Internal members. */
struct aiocb *__next_prio;
int __abs_prio;
int __policy;
int __error_code;
__ssize_t __return_value;
#ifndef __USE_FILE_OFFSET64
__off_t aio_offset; /* File offset. */
char __pad[sizeof (__off64_t) - sizeof (__off_t)];
#else
__off64_t aio_offset; /* File offset. */ //指定的偏移量
#endif
char __glibc_reserved[32];
};其中比较重要的一个字段就是aio_sigevent,具体定义如下:
typedef struct sigevent
{
sigval_t sigev_value;
int sigev_signo;
int sigev_notify;
union
{
int _pad[__SIGEV_PAD_SIZE];
/* When SIGEV_SIGNAL and SIGEV_THREAD_ID set, LWP ID of the
thread to receive the signal. */
__pid_t _tid;
struct
{
void (*_function) (sigval_t); /* Function to start. */
pthread_attr_t *_attribute; /* Thread attributes. */
} _sigev_thread;
} _sigev_un;
} sigevent_t;在进行异步IO前需要先对上述结构体进行初始化。调用aio_read函数来进行异步读操作,调用aio_write函数来进行异步写操作。
#include <aio.h>
extern int aio_read (struct aiocb *__aiocbp) __THROW __nonnull ((1));
extern int aio_write (struct aiocb *__aiocbp) __THROW __nonnull ((1));
当这些函数返回成功时,异步I/O请求便已被操作系统放入等待处理的队列中了。此时仅是请求被放入等待队列中,待实际的I/O操作完成之后,操作系统才会向相关进程发送通知。
14.6 readv和writev函数
readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。函数原型如下:
#include <sys/uio.h>
extern ssize_t readv (int __fd, const struct iovec *__iovec, int __count)
__wur;
extern ssize_t writev (int __fd, const struct iovec *__iovec, int __count)
__wur;
struct iovec定义如下:
struct iovec
{
void *iov_base; /* Pointer to data. */
size_t iov_len; /* Length of data. */
};14.8 存储映射I/O
存储映射I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上,当从缓冲区中读数据时,就相当于读文件中的相应字节,当向缓冲区中写数据时,相应字节就自动写入文件。通过以上方法就可以在不使用read和write的情况下执行I/O。
存储映射函数如下,该函数的功能就是将文件中的一部分映射到存储区中。
extern void *mmap (void *__addr, size_t __len, int __prot,
int __flags, int __fd, __off_t __offset) __THROW;调用mprotect可以更改一个现有映射的权限。
extern int mprotect (void *__addr, size_t __len, int __prot) __THROW;
当进程终止时,会自动解除存储映射区的映射,或者直接调用munmap函数也可以解除映射区。
本文深入探讨了非阻塞I/O的工作原理及其在操作系统中的应用,并介绍了记录锁的功能与使用方法,包括锁的类型、设置及释放过程。此外,还涉及了I/O多路复接技术如select和poll函数的使用,以及存储映射I/O的基本概念。
1308

被折叠的 条评论
为什么被折叠?



