apue高级IO

1、系统调用分成低速系统调用和其他系统调用两类。低速系统调用是可能会使进程永远阻塞的一类调用调用,他们包含:

  • 如果某些文件类型(例如管道,终端设备和网络设备)的数据并不存在,则读操作可能会使调用者永远阻塞。
  • 如果数据不能立即被上述相同类型的文件接受(由于在管道中无空间,网络流控制等),则写操作也会使调用者永远阻塞。
  • 在某些条件发生之前,打开某些类型的文件会被阻塞。(例如开打一个终端设备可能需要等到与之连接的调制解调器应答)
  • 对已经加上强制性记录锁的文件进行读写。
  • 某些ioctl操作。
  • 某些进程间通信函数。

对于一个给定的描述符有两种方法对其指定非阻塞IO:
1.如果调用open获得描述符,则可指定O_NONBLOCK标志。
2.对于已经开打的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态。
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>


char buf[50000];


int main(void){
        int ntow,nw;
        char *ptr;
        ntow = read(STDIN_FILENO,buf,sizeof(buf));
        fprintf(stderr, "read %d bytes.\n",ntow);


        int flags = fcntl(STDOUT_FILENO, F_GETFL,0);
        fcntl(STDOUT_FILENO,F_SETFL,flags|O_NONBLOCK);


        ptr = buf;
        while(ntow > 0){
                errno = 0;
                nw = write(STDOUT_FILENO,ptr,ntow);
                fprintf(stderr,"nwrite = %d, errno = %d\n",nw, errno);


                if(nw > 0){
                        ptr += nw;
                        ntow -= nw;
                }
        }


        fcntl(STDOUT_FILENO,F_SETFL,flags|~O_NONBLOCK);
}
如果输出是普通文件,则可以期望write只执行一次:
./a.out < /etc/termcap >temp.file
read 50000 bytes.
nwrite = 50000, errno = 0

如果标准输出是终端,则期望write有时候会返回小于50000的一个数字,有时则出错返回。
./a.out < /etc/termcap  2>stderr.out
大量输出至终端:
cat stderr.out
read 50000 bytes.
nwrite = 5802, errno = 0
nwrite = -1, errno = 11
.....
nwrite = 6144, errno = 0

2、记录锁  主要为多个进程同时操作同一文件区间而产生 
    fcntl函数原型;
#include<fcntl.h>
int fcntl(int filedes, int cmd, .../* struct flock *flockptr */);
//成功则依赖于cmd,出错则返回-1.
对于记录锁;cmd 是F_GETLK、F_SETLK或F_SETLKW。第三个参数是一个指向flock结构的指针
struct flock{
short l_type; //F_RDLCK,F_WRLCK, or F_UNLCK
off_t l_start; //offset in bytes, relative to l_whence
short l_whence; //SEEK_SET, SEEK_CUR or SEEK_END
off_t l_len; //length in bytes; 0 means lock to EOF
pid_t l_pid; //returned with F_GETLK
};
多个进程在给定的字节上可以有一把读锁,但是在一个给定的字节上只能有一个进程独用一把写锁;
但是对于单个进程,如果一开始对一个文件区间有一把读锁。后来进程又改为写锁,那么新锁将替换老锁。
多路复用:
select:
系统调用为:
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
		fd_set __user *, exp, struct timeval __user *, tvp)
{
	struct timespec end_time, *to = NULL;
	struct timeval tv;
	int ret;

	if (tvp) {
		if (copy_from_user(&tv, tvp, sizeof(tv)))
			return -EFAULT;
由于timerval数据结构都在用户空间,所以要通过copy_from_user把数据从用户空间复制到内核空间中
		to = &end_time;<span lang="EN-US" style="color: rgb(51, 51, 51); // 得到timespec格式的未来超时时间<
		if (poll_select_set_timeout(to,
				tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
				(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
			return -EINVAL;
	}

	ret = core_sys_select(n, inp, outp, exp, to);
	ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
/*如果有超时值, 并拷贝离超时时刻还剩的时间到用户空间的timeval中*/<
	 // 返回就绪的文件描述符的个数

(1)使用copy_from_user从用户空间拷贝timer到内核空间

 (2)core_sys_select ------>do_select

   3、poll_select_copy_remaining

int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
			   fd_set __user *exp, struct timespec *end_time)
{
	fd_set_bits fds;
	void *bits;
	int ret, max_fds;
	unsigned int size;
	struct fdtable *fdt;
	/* Allocate small arguments on the stack to save memory and be faster */
	long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];

	ret = -EINVAL;
	if (n < 0)
		goto out_nofds;

	/* max_fds can increase, so grab it once to avoid race */
	rcu_read_lock();
	fdt = files_fdtable(current->files);// RCU ref, 获取当前进程的文件描述符表
	max_fds = fdt->max_fds;
	rcu_read_unlock();
	if (n > max_fds)// 如果传入的n大于当前进程最大的文件描述符,给予修正
		n = max_fds;

	/*
	 * We need 6 bitmaps (in/out/ex for both incoming and outgoing),
	 * since we used fdset we need to allocate memory in units of
	 * long-words. 操作时需要6个bitmaps  
	 */
	size = FDS_BYTES(n);
	bits = stack_fds;//分配的内存    // 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字
	if (size > sizeof(stack_fds) / 6) { // 除6,为什么?因为每个文件描述符需要6个bitmaps
		/* Not enough space in on-stack array; must use kmalloc */
		ret = -ENOMEM;
		bits = kmalloc(6 * size, GFP_KERNEL);// stack中分配的太小,直接kmalloc
		if (!bits)
			goto out_nofds;
	}
	fds.in      = bits;
	fds.out     = bits +   size;
	fds.ex      = bits + 2*size;
	fds.res_in  = bits + 3*size;
	fds.res_out = bits + 4*size;
	fds.res_ex  = bits + 5*size;
<span style="white-space:pre">	   // get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_set</span>
	if ((ret = get_fd_set(n, inp, fds.in)) ||
	    (ret = get_fd_set(n, outp, fds.out)) ||
	    (ret = get_fd_set(n, exp, fds.ex))) copy到内核空间
		goto out;
/*
 * We do a VERIFY_WRITE here even though we are only reading this time:
 * we'll write to it eventually..
 *
 * Use "unsigned long" accesses to let user-mode fd_set's be long-aligned.
 */
static inline
int get_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset)
{
nr = FDS_BYTES(nr);
if (ufdset)
return copy_from_user(fdset, ufdset, nr) ? -EFAULT : 0;

从用户空间复制到内核空间
<span style="white-space:pre">	</span>memset(fdset, 0, nr);
<span style="white-space:pre">	</span>return 0;
}
	zero_fd_set(n, fds.res_in);// 对这些存放返回状态的字段清0
	zero_fd_set(n, fds.res_out);
	zero_fd_set(n, fds.res_ex);

	ret = do_select(n, &fds, end_time);

	if (ret < 0)
		goto out;
	if (!ret) {   // 超时返回,无设备就绪
		ret = -ERESTARTNOHAND;
		if (signal_pending(current))
			goto out;
		ret = 0;
	}
  // 把结果集,拷贝回用户空间
	if (set_fd_set(n, inp, fds.res_in) ||
	    set_fd_set(n, outp, fds.res_out) ||
	    set_fd_set(n, exp, fds.res_ex))   写回到用户空间  copy_to_user
		ret = -EFAULT;

out:
	if (bits != stack_fds)
		kfree(bits);
out_nofds:
	return ret;<span style="white-space:pre">	</span>返回就绪的文件描述符的个数<span style="white-space:pre">	</span>
</pre><pre code_snippet_id="1555746" snippet_file_name="blog_20160114_5_5606235" name="code" class="cpp">
struct poll_table_entry {
<span style="white-space:pre">	</span>struct file * filp;
<span style="white-space:pre">	</span>wait_queue_t wait;
<span style="white-space:pre">	</span>wait_queue_head_t * wait_address;
};

struct poll_table_page {
<span style="white-space:pre">	</span>struct poll_table_page * next;
<span style="white-space:pre">	</span>struct poll_table_entry * entry;
<span style="white-space:pre">	</span>struct poll_table_entry entries[0];
};
/*
 * Structures and helpers for sys_poll/sys_poll
 */
struct poll_wqueues {
<span style="white-space:pre">	</span>poll_table pt;
<span style="white-space:pre">	</span>struct poll_table_page * table;
<span style="white-space:pre">	</span>int error;
<span style="white-space:pre">	</span>int inline_index;
<span style="white-space:pre">	</span>struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
当一个进程要进入睡眠,而想要某个设备的驱动程序在设备的状态发生变化时将其唤醒,就得要准备好一个wait_queue_ t数据结构,并将这个数据结构挂入目标设备的某个等待队列中。在等待对象单一时一般都把wait_queue_ t数据结构建立在堆栈中。可是,在有多个等待对象时就不能那样了。另一方面,在有多个等待对象、从而有多个wait_queue t数据结构时,要有个既有效、又灵活,便于扩充的方法将这些wait_queue_ t结构管理起来,上面这些数据结构就正是为此而设计的。这里的poll_ table_ entry数据结构既是对wait queue_ t的扩充,又是对它的“包装”。此外,poll_ table_page结构中数组entries []的下标为。,表示该数组的大小可以动态地确定。实际使用时总是分配一个页面,页面中能容纳儿个poll_ table_ entry结构,这个数组就是多大。使用中指针 entry总是指向entries []中的第一个空闲的poll_ table_ entry结构,根据需要动态地分配entries []中的表项。一个页面
用完了,就再分配一个,通过指针next连成一条单链。函数do_ select()中定义了一个局部的poll_ table数据结构table }       poll_initwait(&table);先对其进行初始化
void poll_initwait(struct poll_wqueues *pwq)
{
<span style="white-space:pre">	</span>init_poll_funcptr(&pwq->pt, __pollwait);
<span style="white-space:pre">	</span>pwq->error = 0;
<span style="white-space:pre">	</span>pwq->table = NULL;
<span style="white-space:pre">	</span>pwq->inline_index = 0;初始化还未分配任何页面
}
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{fds为fd_set_bits指针,结构中有6个位图,其中前3个为要求位图; 
	ktime_t expire, *to = NULL;
	struct poll_wqueues table;
	poll_table *wait;
	int retval, i, timed_out = 0;
	unsigned long slack = 0;

	rcu_read_lock();
	retval = max_select_fd(n, fds);通过max_ select fd()根据这3个位图计算出本次操作所涉及最大的已打开文件号是什么,
	rcu_read_unlock();所有号码高于这个数值的已打开文件都与本次操作无关。

	if (retval < 0)
		return retval;
	n = retval;

	poll_initwait(&table);
	wait = &table.pt;
	if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
		wait = NULL; // 如果系统调用带进来的超时时间为0,那么设置 timed_out = 1,表示不阻塞,直接返回。
		timed_out = 1;
	}

	if (end_time && !timed_out)// 超时时间转换
		slack = estimate_accuracy(end_time);

	retval = 0;                                                                                                        CPU就进入了一个无穷for循环,正常情况下一直要到监视中的某个已打开文件中
<span style="white-space:pre">	</span>有了输入或满足了其它等待条件,或者指定的睡眠等待时问已经到期,或者当前进程接收到了信号时
<span style="white-space:pre">	</span>才会结束。
	for (;;) {
		unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;

		set_current_state(TASK_INTERRUPTIBLE);

		inp = fds->in; outp = fds->out; exp = fds->ex;
		rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;

		for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
			unsigned long in, out, ex, all_bits, bit = 1, mask, j;
			unsigned long res_in = 0, res_out = 0, res_ex = 0;
			const struct file_operations *f_op = NULL;
			struct file *file = NULL;
<span style="white-space:pre">	</span>	  // 先取出当前循环周期中的32个文件描述符对应的bitmaps</span>
			in = *inp++; out = *outp++; ex = *exp++;
			all_bits = in | out | ex;
			if (all_bits == 0) {
				i += __NFDBITS;//每32个文件描述符一个循环,正好一个long型数
				continue;
			}
<span style="white-space:pre">	</span>如果三个位图之一中的某一位为1,就对相应的已打开文件作一次询问</span>
			for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
				int fput_needed;
				if (i >= n)
					break;
				if (!(bit & all_bits))
					continue;
				file = fget_light(i, &fput_needed);// 得到file结构指针,并增加<span style="white-space:pre">	</span>并把询问的结果汇集到fds所指的fd_ set bits数据结构中。一趟扫描下来以后,就检查一<<span style="white-space:pre">	</span>下上述的条件是否已经满足,或出了错。如果没有就通过<<span style="white-space:pre">	</span>进入睡眠,到被唤醒时再			在下一轮循环中作另一次扫描。就这样,除第一次以外,以后都是在进程被唤醒时才执行一遍循环,所			以从本质上讲是一种do-while循环。<span style="white-space:pre">	</span>
				if (file) {
					f_op = file->f_op;
					mask = DEFAULT_POLLMASK;
					if (f_op && f_op->poll)<span style="white-space:pre">							在这里循环调用所监测的<内的所有文件描述符对应的驱动程序的</函数	   <span style="white-space:pre">				</span>mask = (*f_op->poll)(file, retval ? NULL : wait);<span style="white-space:pre">	</span>						      /*
                            调用驱动程序中的poll函数,以evdev驱动中的
evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),继续调用__pollwait()回调来分配一个poll_table_entry结构体,该结构体有一个内嵌的等待队列项,设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。
                                          */											 // 释放file结构指针,实际就是减小他的一个引用
计数字段f_count。	</span>
					fput_light(file, fput_needed);
					if ((mask & POLLIN_SET) && (in & bit)) {
						res_in |= bit;<span style="white-space:pre">	</span>如果是这个描述符可读将这个位置位
						retval++;//返回描述符个数加1

					}
					if ((mask & POLLOUT_SET) && (out & bit)) {
						res_out |= bit;
						retval++;
					}
					if ((mask & POLLEX_SET) && (ex & bit)) {
						res_ex |= bit;
						retval++;
					}
				}
			}
			if (res_in)
				*rinp = res_in;
			if (res_out)
				*routp = res_out;
			if (res_ex)
				*rexp = res_ex;          					                       这里的目是为了增加一个抢占点。
                 在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),cond_resched是空操作。
			cond_resched();自动放弃cpu给高级任务使用
		}
		wait = NULL;/ 后续有用,避免重复执行__pollwait()
		if (retval || timed_out || signal_pending(current))
			break;
		if (table.error) {
			retval = table.error;
			break;
		}

		/*
		 * If this is the first loop and we have a timeout
		 * given, then we convert to ktime_t and set the to
		 * pointer to the expiry value.
		 */     /*跳出这个大循环的条件有: 有设备就绪或有异常(retval!=0), 超时(timed_out
              = 1), 或者有中止信号出现*/
		if (end_time && !to) {
			expire = timespec_to_ktime(*end_time);
			to = &expire;
		}
  // 第一次循环中,当前用户进程从这里进入休眠,
// 上面传下来的超时时间只是为了用在睡眠超时这里而已
              // 超时,poll_schedule_timeout()返回0;被唤醒时返回-EINTR
		if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
			timed_out = 1; /* 超时后,将其设置成1,方便后面退出循环返回到上层 */
	}
	__set_current_state(TASK_RUNNING);

	poll_freewait(&table);

	return retval;
}

    那么,在什么情况下会被唤醒呢?首先,受到询问的已打开文件的设备驱动程序会把当前进程通
过一个wait_queue_ t数据结构,从而poll_ table_ entry数据结构,挂入其唤醒队列,使得该设备的中断
服务程序在接收到输入时就会唤醒这个进程。其次,如果指定了时问限制,则当时问到点时也会唤醒
这个进程,这是因为进程在进入睡眠时都指定了需要继续睡眠的时问。最后,如果进程接收到了信号
也会被唤醒。
    显然,这里的关键在于对具体已打开文件,即设备的询问。从代码中可以看出,这是通过具体
file operations数据结构中提供的函数指针poll进行的。我们以前都把注意力集中在open, read, write
等更为常用的操作上,有意忽略了这个函数指针,现在要回过来关注这个操作了。另一方面,阅读poll
操作的代码在某种意义上也是对有关内容的一次复习。
<(2)注册回调函数__pollwait<span style="white-space:pre">											</span>   (3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)<span style="white-space:pre">											</span>          (4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。 l<span style="white-space:pre">						</span>(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。<<span style="white-space:pre">			</span>   (6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。                        (7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。                 (8)把fd_set从内核空间拷贝到用户空间。(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大(3)select支持的文件描述符数量太小了,默认是1024上层要能使用select()和poll()系统调用来监测某个设备文件描述符,那么就必须实现这个设备驱动程序中struct file_operation结构体的poll函数,为什么?因为这两个系统调用最终都会调用驱动程序中的poll函数来初始化一个等待队列项, 然后将其加入到驱动程序中的等待队列头,这样就可以在硬件可读写的时候wake up这个等待队列头,然后等待(可以是多个)同一个硬件设备可读写事件的进程都将被唤醒。(这个等待队列头可以包含多个等待队列项,这些不同的等待队列项是由不同的应用程序调用select或者poll来监测同一个硬件设备的时候调用file_operation的poll函数初始化填充的)。</p>




你可能感兴趣的:(apue高级IO)