前言:
内核态(内核模式)和用户态(用户模式)是linux的一种机制,用于限制应用可以执行的指令和可访问的地址空间,这通过设置某个控制寄存器的位来实现。
进程处于用户模式下,它不允许发起I/O操作,所以它必须通过系统调用进入内核模式才能对文件进行读取。
从用户模式切换到内核模式,主要的开销是处理器要将返回地址(当前指令的下一条指令地址)和额外的处理器状态(寄存器)压入到栈中,这些数据到会被压到内核栈而不是用户栈。
另外,一个进程使用系统调用还隐含了一点——调用系统调用的进程可能会被抢占。
当内核代表用户执行系统调用时,
若该系统调用被阻塞,该进程就会进入休眠,然后由内核选择一个就绪状态,当前优先级最高的进程运行。
另外,即使系统调用没有被阻塞,当系统调用结束,从内核态返回时,若在系统调用期间出现了一个优先级更高的进程,则该进程会抢占使用了系统调用的进程。
内核态返回会返回到优先级高的进程,而不是原本的进程。
一、Linux下基本的I/O介绍:
ssize_t read(int fd, void *buf, size_tcount);
ssize_t write(int fd, void *buf, size_tcount);
在Linux中,read 和write 是基本的系统级I/O函数。当用户进程使用read 和 write 读写linux的文件时,进程会从用户态进入内核态,通过I/O操作读取文件中的数据。这样每次读写都通过系统调用会增大系统的负担的。
二、介绍一款在《深入理解计算机系统》中看到的RIO包:
RIO,全称Robust I/O,即健壮的IO包。它提供了与系统I/O类似的函数接口,在读取操作时,RIO包加入了读缓冲区,一定程度上增加了程序的读取效率。
首先有rio_t这个结构体,是一个读缓冲区的格式:
#define RIO_BUFSIZE 4096
typedefstruct
{
int rio_fd; //与缓冲区绑定的文件描述符的编号
int rio_cnt; //缓冲区中还未读取的字节数
char *rio_bufptr; //当前下一个未读取字符的地址
char rio_buf[RIO_BUFSIZE];
}rio_t;
这个是rio的数据结构,通过rio_readinitb(rio_t *, int)可以将文件描述符与rio数据结构绑定起来。注意到这里的rio_buf的大小是4096,为linux中文件的块大小。
初始化读缓冲区
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
return;
}
带缓冲区的读函数
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
int cnt;
while(rp->rio_cnt <= 0)
{
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));
if(rp->rio_cnt < 0)
{
if(errno != EINTR) //遇到中断类型错误的话应该进行读取,否则就返回错误
return -1;
}
elseif(rp->rio_cnt == 0) //读取到了EOF
return0;
else
rp->rio_bufptr = rp->rio_buf; //重置bufptr指针,令其指向第一个未读取字节,然后便退出循环
}
cnt = n;
if((size_t)rp->rio_cnt < n)
cnt = rp->rio_cnt;
memcpy(usrbuf, rp->rio_bufptr, n);
rp->rio_bufptr += cnt; //读取后需要更新指针
rp->rio_cnt -= cnt; //未读取字节也会减少
return cnt;
}
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
size_t leftcnt = n;
ssize_t nread;
char *buf = (char *)usrbuf;
while(leftcnt > 0)
{
if((nread = rio_read(rp, buf, n)) < 0)
{
if(errno == EINTR) //其实这里可以不用判断EINTR,rio_read()中已经对其处理了
nread = 0;
else
return -1;
}
leftcnt -= nread;
buf += nread;
}
return n-leftcnt;
}
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{
size_t n;
int rd;
char c, *bufp = (char *)usrbuf;
for(n=1; n<maxlen; n++) //n代表已接收字符的数量
{
if((rd=rio_read(rp, &c, 1)) == 1)
{
*bufp++ = c;
if(c == '\n')
break;
}
elseif(rd == 0) //没有接收到数据
{
if(n == 1) //如果第一次循环就没接收到数据,则代表无数据可接收
return0;
else
break;
}
else
return -1;
}
*bufp = 0;
return n;
}
以上三个的读函数都是带缓冲区的,但是下面这个rio_writen不需要写缓冲。
因为,比如说,我们在写一个http的请求报文,然后将这个报文写入了对应socket的文件描述符的缓冲区,假设缓冲区大小为8K,该请求报文大小为1K。那么,如果缓冲区被设置为被填满才会自动将其真正写入文件(而且一般也是这样做的),那就是说如果没有提供一个刷新缓冲区的函数手动刷新,我还需要额外发送7K的数据将缓冲区填满,这个请求报文才能真正被写入到socket当中。所以,一般带有缓冲区的函数库都会一个刷新缓冲区的函数,用于将在缓冲区的数据真正写入文件当中,即使缓冲区没有被填满,而这也是C标准库的做法。然而,如果一个程序员一不小心忘记在写入操作完成后手动刷新,那么该数据(请求报文)便一直驻留在缓冲区,而你的进程还在傻傻地等待响应。
因此,下面那个函数的fd就是int型的。
ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nwritten;
char *bufp = (char *)usrbuf;
while(nleft > 0)
{
if((nwritten = write(fd, bufp, nleft))<= 0)
{
if(errno == EINTR)
nwritten = 0;
else
return -1;
}
bufp += nwritten;
nleft -= nwritten;
}
return n;
}