文件IO


Linux 一切皆文件



通用文件读写过程


文件描述符的实质是进程的文件描述符表中的已打开/创建文件的索引


在这里插入图片描述


1.打开文件

#include <fcntl.h>
/**
 * 作用:打开文件
 * 参数:文件名 + 文件状态 + 文件权限
 * 返回值类型:文件描述符(由打开该文件的进程管理)
 */
int open(const char *__file, int __oflag, ...);


2.读写文件

#include <unistd.h>
/**
 * 作用:读写文件
 * 参数:文件描述符 + 缓冲区 + 读写字节数
 * 返回值类型:读写字节数
 */
ssize_t read(int __fd, void *__buf, size_t __nbytes);
ssize_t write(int __fd, const void *__buf, size_t __n);


3.关闭文件

#include <unistd.h>
/**
 * 作用:关闭文件
 * 参数:文件描述符
 * 返回值类型:正确/错误
 */
int close(int __fd);


文件读写过程

在这里插入图片描述


用户进程(用户缓冲区)<——> 内核缓冲区 <——> 硬件设备

(1)读文件

  • 用户进程向系统进程发出读文件请求
  • 系统进程读取文件内容到用户进程文件缓存区
  • 用户进程等待缓冲区数据填满然后读取

(2)写文件

  • 用户进程向系统进程发出读文件请求
  • 用户进程将写文件内容输出到用户进程文件缓存区
  • 系统进程读取缓存区内容然后写入到文件中


字节流,读写文件内容时,通过指针读写字节

#include <unistd.h>
#include <sys/type.h>

/**
 * 作用:改变文件指针位置
 * 参数:文件描述符 + 文件指针偏移量 + 文件指针起始偏移位置(当前、文件头、文件尾)
 * 返回值类型:文件指针位置相较于文件头偏移量
 */
whence:SEEK_CURSEEK_SETSEEK_END;
off_t lseek(int fd, off_t offset, int whence);


文件IO方式

(1)

缓冲IO

与非缓冲IO

  • 标准库函数使用缓冲IO,

    减少系统调用

    时间
  • 标准库函数将读取到的文件内容保存在流缓存中,然后再经过系统调用到内核缓存中

(2)直接IO与非直接IO

  • 直接IO:绕过缓存区高速缓存,直接读写设备
  • 需要遵守许多限制,而且很大程度上会慢于非直接IO

(3)阻塞IO与

非阻塞IO

  • 阻塞IO:读写文件时,进程阻塞在读写文件过程中,若无数据(发生错误)则会一直阻塞
  • 非阻塞IO:发生错误时不会一直阻塞在读写文件过程中,

    没有数据到达,立即返回

(4)同步IO与异步IO

  • 同步IO:阻塞IO与非阻塞IO都属于同步IO

    • 数据准备过程(将数据保存在缓存区)
    • 数据读写过程(缓存区数据刷新)
    • 阻塞IO阻塞在两个过程,非阻塞IO阻塞在第二个过程
  • 异步IO:数据准备与读写过程都不阻塞
#include <stdio.h>
#include <fcntl.h>

FILE *fopen(const char *pathname, const char *mode);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fclose(FILE *stream);

int fcntl(int fd, int cmd, ... /* arg */ );
cmd:F_GETFL、F_SETFL

int flag = fcntl(fd,F_GETFL);
flag = flag | _O_DIRECT | O_NONBLOCK;
fcntl(fd,F_SETFL,flag);



IO多路复用


通过事件触发,然后通知用户进程(线程)执行IO操作


1.事件触发方式

(1)水平触发:当IO处于

就绪状态

(非阻塞读写IO)时,通知用户进程(线程)执行IO操作

(2)

边缘触发

:当有

IO事件

来临时,通知用户进程(线程)执行IO操作


2.IO复用分类

(1)select:

轮询有限文件描述符

,查询IO状态



  • 文件描述符集合

    从用户进程

    拷贝

    到内核进程,由内核进程

    监控文件状态

    ,然后修改集合
  • 用户进程

    查询

    文件描述符集合查看哪些文件描述符经过修改,然后修改集合
  • 重复以上操作

//初始化描述符集合
void FD_ZERO(fd_set *set);
//添加监听描述符
void FD_SET(int fd, fd_set *set);
//检测描述符是否发生改变
int  FD_ISSET(int fd, fd_set *set);
//清除监听描述符
void FD_CLR(int fd, fd_set *set);

struct timeval {
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* microseconds */
};

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
/**
 * 创建 select
 * nfds:监听文件描述符 readfds、writefds、exceptfds:读、写、错误描述符集合
 * timeout:超时处理
 * /
#include <sys/select.h>
#include <time.h>
int main(int argc,char *argv[])
{
	//超时时间
	struct timeval_t timeout;
	timeout.tv_sec = 5;
	timeout.tv_usec = 5000;
	//创建文件描述符集合
	fd_set readfds,writefds,errorfds;
	//初始化为0
	FD_ZERO(&readfds);
	FD_ZERO(&writefds);
	FD_ZERO(&errorfds);
	//加入文件描述符
	int readfd,writefd,errorfd;
	//将文件描述符加入
	FD_SET(readfd,&readfds);
	FD_SET(writefd,&writefds):
	FD_SET(errorfd,&errorfds);
	//开始查询
	int fd_max = (readfd > writefd) ? ((readfd > errorfd) ? readfd : errorfd) : ((writefd > errorfd) ? writefd : errorfd)
	select(fd+1,&readfds,&writefds,&errorfds,&timeout);
	
	for(int i = 0;i < fd+1;i++)
	{
		if(FD_ISSET(i,&readfds) || (FD_ISSET(i,&writefds) || (FD_ISSET(i,&errorfds))
		{
			//...
		}
	}
	
}

(2)poll:

轮询文件描述符

,查询IO状态

  • 链式存储
  • 监听事件结构体
  • 监听次序与 select 相同
struct pollfd {
	int   fd;         /* file descriptor */
	short events;     /* requested events */
	short revents;    /* returned events */
};

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
 * 创建 poll
 * fds:事件集合 nfds:监听事件个数
 * timeout:超时处理
 * /
#define _GNU_SOURCE_
#include <sys/poll.h>
#define MAX_SIZE
int main()
{
	int count = 0,i = 0;
	struct pollfd fds[MAX_SIZE];
	fds[0].fd = serv_sock;
	fds[0].events = POLLIN;
	fds[0].revents = 0;
	
	while(1)
	{
		//每次添加事件都需要进行拷贝(拷贝两次)
		poll(fds,count+1,1000);
		for(i = 0;i < count + 1;i++)
		{
			if(fds[i].revents & POLLRDHUP || fds[i].revents & POLLERR)
			{
				fds[i] = fds[count];
				count--;
				close(fds[i].fd];
			}
			else if(fds[i].fd == serv_sock && fds[i].revents == POLLIN)
			{
				//接收 client socket
				fds[++count].fd = clnt_sock;
				fds[count].events = POLLIN | POLLERR;
				fds[count].revents = 0;
			}
			else if(fds[i].revents & POLLIN)
			{
				//读写客户端
				fds[i].revents &= ~POLLIN;
			}
		}
	}
	return 0;
}


(3)epoll

:通过函数回调


  • 红黑树 + 链式队列

    (快速查找 + 响应队列)
  • 事件触发后将触发事件通过

    函数回调

    通知进程处理
  • 支持边缘触发
  • 不需要将所有待监听的描述符都加入,只需将监听的描述符加入即可
struct epoll_event {
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};

//创建 epoll 
int epoll_create(int size);
//操作 epoll 事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//监听 事件响应
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
#include <sys/epoll.h>
#define MAX_SIZE 1024
int main(int argc,char *argv[])
{
	//创建服务器端套接字
	//待监听事件
	struct epoll_event ev;
	//返回响应事件
	struct epoll_event[MAX_SIZE];

	//创建 epoll 
	int efd = epoll_create(MAX_SIZE);
	//将服务器端套接字加入红黑树
	ev.data.fd = serv_sock;
	ev.events = EPOLLIN;
	epoll_ctl(efd,EPOLL_CTL_ADD,serv_sock,&ev);

	//开始监听
	while(1)
	{
		int nfd = epoll_wait(efd,events,MAX_SIZE,1000);
		for(int i = 0;i < nfd;i++)
		{
			//发生错误或客户端断开
			if(events[i].events & EPOLLRDHUP || events[i].events & EPOLLERR)
			{
				epoll_ctl(efd,EPOLL_CTL_DEL,events[i].data.fd,&ev);
				close(events[i].data.fd);
			}
			else if(events[i].data.fd == serv_sock && events[i].events & EPOLLIN)
			{
				//接收客户端请求
				//将客户端套接字加入红黑树
				ev.data.fd = clnt_sock;
				ev.events = EPOLLIN | EPOLLERR;
				epoll_ctl(efd,EPOLL_CTL_ADD,clnt_sock,&ev);
			}
			else if(events[i].events & EPOLLIN)
			{
				//读写数据
			}
		}
	}
	
}



版权声明:本文为weixin_45366970原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_45366970/article/details/128077932