Linux--深入了解IO

时间:2021-04-28 17:36:52

五种IO模型


阻塞IO:在内核将数据准备好之前,系统调用会一直等待,所有的套接字默认是阻塞方式。
非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。(阻塞IO往往需要用循环的方式反复尝试读写文件描述符,这个过程称为轮询,这样会大量浪费CPU,只有在特定的场景下才会使用)
信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
IO多路转接(多路复用):和阻塞IO类似,最核心在于IO多路转接能够同时等待多个文件描述符。
异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝)

同步通信VS异步通信

消息通信机制
同步:在发出一个调用时,在没有得到结果之前,该调用就不返回,但一旦调用返回,就得到返回值了。这是由调用者主动等待调用的结果。

异步:在调用发出后,这个调用直接返回,所以没有返回结果,但调用发生后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

回顾:线程–同步与互斥

阻塞与非阻塞

程序在等待调用结果(消息,返回值)时的状态
阻塞调用:调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
非阻塞调用:在不能立刻得到结果之前,该调用不会阻塞当前进程。

非阻塞IO


fcntl:一个文件描述符,默认都是阻塞IO

函数原型

#include<unistd.h>
#include<fcntl.h>

int fcntl(int fd,int cmd,.../*arg*/);

传入的cmd的值不同,后面追加的参数也不同。
fcntl函数的6种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD)
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
  • 获得/设置异步IO所有权(cmd=F_GETOWN或F_SETOWN)
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

使用第三种功能,获得/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。

实现SetNoBlock函数

void SetNoBlock(int fd)
{
       int f1 = fcntl(fd,F_GETFL);
       if(f1<0)
       {
              perror("fcntl");
              return;
       }
       fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
}
  • 使用F_GETFL将当前文件描述符的属性取出来。
  • 然后使用F_SETFL将文件描述符设置回去,并加上一个O_NONBLOCK属性。

轮询方式读取标准输入

#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>

void SetNoBlock(int fd)
{
       int f1 = fcntl(fd,F_GETFL);
       if(f1<0)
       {
              perror("fcntl");
              return;
       }
       fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
}

int main()
{
       SetNoBlock(0);
       while(1)
       {
              char buf[1024] = {0};
              ssize_t read_size = read(0,buf,sizeof(buf)-1);
              if(read_size<0)
              {
                     perror("read");
                     sleep(1);
                     continue;
              }
              printf("input:%s\n",buf);
       }
       return 0;
}

Linux--深入了解IO

重定向


dup/dup2系统调用

函数原型

#include<unistd.h>

int dup(int oldfd);
int dup2(int oldfd,int newfd);

使用dup将标准输出重定向到文件中

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
     int fd = open("./log", O_CREAT | O_RDWR);

     if (fd < 0) 
     {
         perror("open");
         return 1;
     }
     close(1);//关闭文件描述符1

     int new_fd = dup(fd);//进行重定向操作
     if (new_fd != 1) 
     {
         perror("dup");
         return 1;
     }
     printf("new_fd: %d\n", new_fd);
     close(fd);//关闭原来的文件描述符,利用文件描述符分配规则使new_fd完成分配

     for (;;) 
     {
         char buf[1024] = { 0 };
         ssize_t read_size = read(0, buf, sizeof(buf) - 1);
         if (read_size < 0) 
         {
             perror("read");
             continue;
         }
         printf("%s", buf);
         fflush(stdout);
     }
     close(new_fd);
     return 0;

}

利用了dup分配文件描述符是从最小的开始分配这样的机制, 来完成这个重定向。

使用dup2将标准输出0.重定向到文件中

#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>


int main()
{
       int fd = open("./log",O_CREAT | O_RDWR);
       if(fd < 0)
       {
              perror("open");
              return 1;
       }
       close(1);

       dup2(fd,1);//进行写操作的重定向
       for(;;)
       {
              char buf[1024] = {0};
              ssize_t read_size = read(0,buf,sizeof(buf)-1);
              if(read_size < 0)
              {
                     perror("read");
                     break;
              }
              printf("%s",buf);
              fflush(stdout);
       }
       return 0;

}

IO多路转接之select


系统提供select函数来实现多路复用输入/输出模型

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化。
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了变化。

select函数原型

#include<sys/select.h>

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

参数

  1. nfds是需要监视的最大文件描述符+1。
  2. rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合。
  3. 参数timeout为结构timeval,用来设置select()的等待时间。

timeout取值

  1. NULL:表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
  2. 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  3. 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

关于fd_set结构

这个结构就是一个整数结构,更严格地说,是一个“位图”,使用位图中对应的位来表示要监视的文件描述符。
提供了一组操作fd_set的接口,来比较方便的操作位图:

void FD_CLR(int fd, fd_set *set); // ⽤用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // ⽤用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // ⽤用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // ⽤用来清除描述词组set的全部位

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

select执行过程

理解select模型关键在理解fd_set,为了说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd.则1字节长的fd_set最大可以对应8个fd。

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对⼀一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

select的特点

  • 可监控的文件描述符个数取决与sizeof(fdset) 的值 ,一个服务器上 sizeof(fdset)=512,每bit表⽰示一个文件描述符,则服务器上支持的最大文件描述符是512*8=4096.
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。
    1.一是用于再select 返回后,array作为源数据和fdset 进行 FDISSET判断。
    2.select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取
    得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

select缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便。
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
  • select支持的文件描述符数量太小。