第14章 高级I/O

14.2 非阻塞I/O (无法进行I/O操作时直接出错返回)

(1)可将系统调用分成两类:

1)“低速”系统调用:可能会使进程永远阻塞的一类系统调用。<388>

2)其他。


(2)对一个给定的描述符,指定非阻塞I/O的方法。(两种)

1)调用open函数时,指定O_NONBLOCK标志。

2)调用fcntl函数,打开描述符的O_NONBLOCK标志。


(3)实例:长的非阻塞write

#include "apue.h"
#include <errno.h>
#include <fcntl.h>

char	buf[500000];

int
main(void)
{
	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);	/* set nonblocking */

	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;
		}
	}

	clr_fl(STDOUT_FILENO, O_NONBLOCK);	/* clear nonblocking */

	exit(0);
}
void
set_fl(int fd, int flags) /* flags are file status flags to turn on */
{
	int		val;

	if ((val = fcntl(fd, F_GETFL, 0)) < 0)    //获得当前文件状态标志
		err_sys("fcntl F_GETFL error");

	val |= flags;		/* turn on flags */ //修改标志

	if (fcntl(fd, F_SETFL, val) < 0)    //设置标志
		err_sys("fcntl F_SETFL error");
}

1)set_fl函数:对一个文件描述符设置一个或多个文件状态标志的函数。

      clr_fl函数:清除。

2)标准输出是普通文件和终端时的区别。

    普通文件:可以期望write只执行一次。

    终端:write执行多次,有时返回错误。

原因:终端驱动程序一次能接受的数据量随系统而变。

3)轮询:这种形式的循环称作轮询,在多用户系统上会浪费CPU时间。

4)多线程可避免使用非阻塞I/O。可以允许单个线程在I/O调用中阻塞,其他线程中执行其它任务。

14.3 记录锁

(1)记录锁的功能:

当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。

(一个更适合的术语:字节范围锁。因为它锁定的只是文件中的一个区域(也可能是整个文件))


fcntl记录锁:

(2)请求锁的一些规则。

1)不同进程提出的锁请求,不同类型锁彼此之间的间的兼容性。(共享读锁和独占性写锁)

2)单个进程提出的多个锁请求,新锁将替换已有锁。

3)请求一把锁时对描述符的要求:

加读锁时,该描述符必须是读打开。加写锁时,该描述符必须是写打开。


(3)fcntl函数3个关于记录锁的命令:

F_GETLK:测试能否建立一把锁。

F_SETLK:企图建议一把锁。

F_SETLKW:企图建议一把锁。(阻塞版本)


(4)饿死:频繁的加读锁的请求,使一个文件区间始终存在一把或几把读锁,导致欲加写锁的进程等待很长时间。

    

(5)组合,分裂

在设置或释放文件上的一把锁时,系统按要求组合或分裂相邻区。


(6)实例1

1)定义一个lock_reg函数,此函数加锁或解锁一个文件区域,避免每次分配flock结构,然后填入各项信息。

2)将特定的加锁或解锁一个文件区域定义成宏。


(7)实例2:测试一把锁

1)定义了一个lock_test函数用于测试一把锁。(基于fcntl函数,并用宏来调用)

2)不能用此函数测试自己是否在文件的某一部分持有一把锁。(单个进程提出多个锁请求的替换规则)


(8)实例3:死锁

1)什么时候两个进程处于死锁状态:两个进程互相等待对方持有并且不释放(锁定)的资源时。

2)什么时候有发生死锁的可能性:一个进程已经控制了文件的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,那么它就会休眠。

3)检测到死锁时,内核必须选择一个进程接收出错返回。

#include "apue.h"
#include <fcntl.h>

static void
lockabyte(const char *name, int fd, off_t offset)
{
	if (writew_lock(fd, offset, SEEK_SET, 1) < 0)
		err_sys("%s: writew_lock error", name);
	printf("%s: got the lock, byte %lld\n", name, (long long)offset);
}

int
main(void)
{
	int		fd;
	pid_t	pid;

	/*
	 * Create a file and write two bytes to it.
	 */
	if ((fd = creat("templock", FILE_MODE)) < 0)
		err_sys("creat error");
	if (write(fd, "ab", 2) != 2)
		err_sys("write error");

	TELL_WAIT();
	if ((pid = fork()) < 0) {
		err_sys("fork error");
	} else if (pid == 0) {			/* child */
		lockabyte("child", fd, 0);
		TELL_PARENT(getppid());
		WAIT_PARENT();
		lockabyte("child", fd, 1);
	} else {						/* parent */
		lockabyte("parent", fd, 1);
		TELL_CHILD(pid);
		WAIT_CHILD();
		lockabyte("parent", fd, 0);
	}
	exit(0);
}


锁的隐含继承和释放

(9)记录锁的自动继承和释放的3条规则。

1)锁与进程和文件两者相关联。(进程终止锁释放,描述符关闭锁释放)

2)fork产生的子进程不继承父进程所设置的锁。(锁是阻止多个进程同时写一个文件)

3)执行exec后,新程序可以继承原执行程序的锁。


456没看

14.4 I/O多路转接(先看一下那些描述符可以使用,再调用I/O函数,确保I/O操作一定能进行)

(1)

1)当从一个描述符读,然后又写到另一个描述符时,可以在循环中使用阻塞I/O。

2)当从两个描述符读,不能在任一个描述符上进行阻塞读,否则导致另一个描述符即使有数据也无法处理。


(2)5中解决此问题的技术。

1)使用两个进程。

2)使用两个线程。

3)使用非阻塞I/O,轮询。(浪费CPU时间,在多任务系统中应当避免使用)

4)异步I/O。进程告诉内核:当描述符准备好可以进行I/O时,用一个信号通知它。

5)I/O多路转接。(比较好)


14.4.1 函数select和pselect

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);


(1)使用select函数的返回信息,调用相应的I/O函数(write或read),可确知该函数不会阻塞。


(2)select函数的参数

1)timeout:指定愿意等待的时间长度。

2)readfds,writefds,exceptfds:指向描述符集的指针。(我们关心的可读,可写或处于异常条件的描述符集)

3)fd_set类型可进行的操作:

    1.分配一个这种类型的变量,将这种类型的一个变量值赋值给同类型的另一个变量。

    2.

           void FD_CLR(int fd, fd_set *set);

       int  FD_ISSET(int fd, fd_set *set);

       void FD_SET(int fd, fd_set *set);

       void FD_ZERO(fd_set *set);

4)nfds:“最大文件描述符编号值加1” 或 要检查的描述符数。(描述符编号从0开始)

通过指定我们所关注的最大描述符,内核就只需在此范围内寻找打开的位。


(3)select函数的返回值

-1:出错。

0:没有描述符准备好。

一个正值:已准备好的描述符数之和。

(描述符集中仍旧打开的位对应于已准备好的描述符)(通过修改参数指示哪一个描述符已准备好)


(4)什么是描述符”准备好“的概念。<407>


(5)影响select是否阻塞的因素。

1)设定的愿意等待的时间长度。

2)是否捕捉到一个信号。

(描述符阻塞与否并不影响select是否阻塞)


(6)描述符到达了文件尾端,则select会认为该描述符是可读的。


(7)pselect与select的区别。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);

1)timespec比timeval提供更精准的超时时间。

2)timeout声明为const。

3)以原子方式安装信号屏蔽字。

14.4.2 poll函数

(1)int poll(struct pollfd *fds, nfds_t nfds, int timeout);

1)poll函数构造一个pollfd结构的数组。每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。

2)nfds:指定数组中的元素数。

3)通过设置结构中的成员


(2)文件尾端,挂断之间的区别。


(3)select和poll中断后不重启。

14.5 异步I/O(提出I/O请求就可以了,程序可以去做其他事情了,I/O操作在某个时刻会完成)

(1)POSIX异步I/O接口使用AIO控制块来描述I/O操作。

aiocb结构定义了AIO控制块。


(2)异步I/O接口不影响操作系统维护的文件偏移量。

不要在同一个进程里把异步I/O函数和传统I/O函数混在一起用在同一个文件上。


(3)aio_read函数进行异步读操作,aio_write函数进行异步写操作。

它们返回成功时,异步I/O请求便已经被操作系统放入等待处理的队列。


(4)I/O操作在等待时,必须确保AIO控制块和数据库缓冲区保持稳定;它们下面对应的内存必须始终是合法的,除非I/O操作完成,否则不能被复用。


实例:将一个文件从一种格式翻译成另一种格式。

(1)非异步实现:循环调用read,write。

#include "apue.h"
#include <ctype.h>
#include <fcntl.h>

#define BSZ 4096

unsigned char buf[BSZ];

unsigned char
translate(unsigned char c)
{
	if (isalpha(c)) {
		if (c >= 'n')
			c -= 13;
		else if (c >= 'a')
			c += 13;
		else if (c >= 'N')
			c -= 13;
		else
			c += 13;
	}
	return(c);
}

int
main(int argc, char* argv[])
{
	int	ifd, ofd, i, n, nw;

	if (argc != 3)
		err_quit("usage: rot13 infile outfile");
	if ((ifd = open(argv[1], O_RDONLY)) < 0)
		err_sys("can't open %s", argv[1]);
	if ((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
		err_sys("can't create %s", argv[2]);

	while ((n = read(ifd, buf, BSZ)) > 0) {
		for (i = 0; i < n; i++)
			buf[i] = translate(buf[i]);
		if ((nw = write(ofd, buf, n)) != n) {
			if (nw < 0)
				err_sys("write failed");
			else
				err_quit("short write (%d/%d)", nw, n);
		}
	}

	fsync(ofd);
	exit(0);
}

1)int fsync(int fd);

此函数保证磁盘上实际文件系统与缓冲区中内容的一致性。(把延迟写数据块写入磁盘)


(2)使用异步I/O实现。

#include "apue.h"
#include <ctype.h>
#include <fcntl.h>
#include <aio.h>
#include <errno.h>

#define BSZ 4096
#define NBUF 8

enum rwop {
	UNUSED = 0,
	READ_PENDING = 1,
	WRITE_PENDING = 2
};

struct buf {
	enum rwop     op;
	int           last;
	struct aiocb  aiocb;
	unsigned char data[BSZ];
};

struct buf bufs[NBUF];

unsigned char
translate(unsigned char c)
{
	/* same as before */
	if (isalpha(c)) {
		if (c >= 'n')
			c -= 13;
		else if (c >= 'a')
			c += 13;
		else if (c >= 'N')
			c -= 13;
		else
			c += 13;
	}
	return(c);
}

int
main(int argc, char* argv[])
{
	int					ifd, ofd, i, j, n, err, numop;
	struct stat			sbuf;
	const struct aiocb	*aiolist[NBUF];    //用于aio_suspend
	off_t				off = 0;    //所读文件的偏移,写入输出文件时使用相同的偏移

	if (argc != 3)
		err_quit("usage: rot13 infile outfile");
	if ((ifd = open(argv[1], O_RDONLY)) < 0)
		err_sys("can't open %s", argv[1]);
	if ((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
		err_sys("can't create %s", argv[2]);
	if (fstat(ifd, &sbuf) < 0)
		err_sys("fstat failed");

	/* initialize the buffers */
	for (i = 0; i < NBUF; i++) {
		bufs[i].op = UNUSED;
		bufs[i].aiocb.aio_buf = bufs[i].data;
		bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE;
		aiolist[i] = NULL;
	}

	numop = 0;    //    用于记录当前还未执行完的异步操作数
	for (;;) {
		for (i = 0; i < NBUF; i++) {
			switch (bufs[i].op) {
			case UNUSED:
				/*
				 * Read from the input file if more data
				 * remains unread.
				 */
				if (off < sbuf.st_size) {
					bufs[i].op = READ_PENDING;
					bufs[i].aiocb.aio_fildes = ifd;
					bufs[i].aiocb.aio_offset = off;
					off += BSZ;
					if (off >= sbuf.st_size)
						bufs[i].last = 1;
					bufs[i].aiocb.aio_nbytes = BSZ;
					if (aio_read(&bufs[i].aiocb) < 0)
						err_sys("aio_read failed");
					aiolist[i] = &bufs[i].aiocb;
					numop++;
				}
				break;

			case READ_PENDING:
				if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
					continue;
				if (err != 0) {
					if (err == -1)
						err_sys("aio_error failed");
					else
						err_exit(err, "read failed");
				}

				/*
				 * A read is complete; translate the buffer
				 * and write it.
				 */
				if ((n = aio_return(&bufs[i].aiocb)) < 0)
					err_sys("aio_return failed");
				if (n != BSZ && !bufs[i].last)
					err_quit("short read (%d/%d)", n, BSZ);
				for (j = 0; j < n; j++)
					bufs[i].data[j] = translate(bufs[i].data[j]);
				bufs[i].op = WRITE_PENDING;
				bufs[i].aiocb.aio_fildes = ofd;
				bufs[i].aiocb.aio_nbytes = n;
				if (aio_write(&bufs[i].aiocb) < 0)
					err_sys("aio_write failed");
				/* retain our spot in aiolist */
				break;

			case WRITE_PENDING:
				if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
					continue;
				if (err != 0) {
					if (err == -1)
						err_sys("aio_error failed");
					else
						err_exit(err, "write failed");
				}

				/*
				 * A write is complete; mark the buffer as unused.
				 */
				if ((n = aio_return(&bufs[i].aiocb)) < 0)
					err_sys("aio_return failed");
				if (n != bufs[i].aiocb.aio_nbytes)
					err_quit("short write (%d/%d)", n, BSZ);
				aiolist[i] = NULL;
				bufs[i].op = UNUSED;
				numop--;
				break;
			}
		}
		if (numop == 0) {
			if (off >= sbuf.st_size)
				break;
		} else {
			if (aio_suspend(aiolist, NBUF, NULL) < 0)
				err_sys("aio_suspend failed");
		}
	}

	bufs[0].aiocb.aio_fildes = ofd;
	if (aio_fsync(O_SYNC, &bufs[0].aiocb) < 0)
		err_sys("aio_fsync failed");
	exit(0);
}

1)定义的struct buf包含一下信息。

struct buf {
	enum rwop     op;    //此结构体正在用于数目异步操作
	int           last;    //输入文件是否读完
	struct aiocb  aiocb;    //AIO控制块
	unsigned char data[BSZ];    //数据缓冲区
};


2)int fstat(int fd, struct stat *buf);

此函数获得已在描述符fd上打开文件的有关信息。struct stat用于存储这些信息。

这里主要为了得到输入文件的大小。


3)int aio_error(const struct aiocb *aiocbp);

获知一个异步读,写或者同步操作的完成状态。


4)ssize_t aio_return(struct aiocb *aiocbp);

异步操作成功,则可调用此函数来获取异步操作的返回值(read,write或fsync在被成功调用时可能返回的结果)。


5)int aio_suspend(const struct aiocb * const aiocb_list[],  int nitems, const struct timespec *timeout);

阻塞进程,直到异步操作完成。


6) int aio_fsync(int op, struct aiocb *aiocbp);

强制所有等待中的异步操作不等待而写入持久化的存储中。

14.6 函数readv和writev(散步读,聚集写)

(1)功能:在一次函数调用中读,写多个非连续缓冲区。


(2)实例1:将两个缓冲区的内容连续地写到一个文件中。

3种方法比较:

1)调用两次write

2)分配一个新缓冲区,复制,调用一次write

3)调用writev


(3)应当用尽量少的系统调用次数来完成任务。

14.7 函数readn和writen(由作者定义)

(1)功能:分别读,写指定的N字节数据,并处理返回值可能小于要求值的情况。


(2)为什么要定义这两个函数:

对管道,FIFO,以及某些设备(特别是终端和网络),read,write可能返回少于所要求的数据。(读写磁盘文件可能不会有这种情况)







你可能感兴趣的:(第14章 高级I/O)