linux的I/O模型

linux的I/O模型

一、简介

这里介绍下linux的I/O模型。以linux內核2.6.39版本源码为基础,先介绍与Linux的I/O模型相关的基础知识,包括内核空间和用户空间、linux文件、linux进程以及并发,接着正式介绍Linux的I/O模型,最后扩展jdk中I/O和linux中查看系统资源限制。

二、内核空间与用户空间

linux将内存分为内核空间和用户空间。

2.1 内核空间

内核空间通常位于高字节的内存空间,供内核使用。内核拥有访问外设(底层硬件的权限)。

2.2 用户空间

用户空间通常位于低字节的内存空间,供应用程序(各种用户进程)使用,没有直接访问外设的权限。因此用户进程要读取外设数据,需要借助内核,数据先复制到内核的缓冲区,再由内核的缓冲区复制到用户进程。

2.3 IO流程

IO操作将基于内核空间和用户空间,这里以读取数据为例:

读取流程如下:

  1. 数据从外设复制到内核缓冲区;
  2. 数据再从内核缓冲区读入用户进程;

示意图如下:

数据
数据
外设
内核缓冲区
用户进程

三、linux文件

3.1 linux中I/O操作的文件

linux中一切(包括外设等)都是文件,linux内核会为这些文件创建文件描述符(file descriptor,fd),所有的I/O操作都是基于文件描述符来执行的。文件描述符其实就是一个数字,指向内核中的一个结构体(包含文件路径、数据区等信息)。如linux中,0表示标准输入,1表示标准输出,2表示标准错误。

3.1.1 文件描述符(fd, file descriptor)

进程访问文件,会触发系统调用,返回文件描述符fd(file descriptor,即是对打开文件的对象引用,其实就是一个int值),定义如下:

//打开文件
fd = open(path, flag, mode)

其中open方法的参数解释如下:
path:表示文件路径(相对或绝对);
flag:文件打开方式(读、写、读/写、追加),也可新建文件;
mode:新创建文件的访问权限;
open方法在源码完整定义如下:

//打开文件并返回新的文件描述符fd(位于/usr/include/fcntl.h的157行)
/* Open FILE and return a new file descriptor for it, or -1 on error.
   OFLAG determines the type of access used.  If O_CREAT or O_TMPFILE is set
   in OFLAG, the third argument is taken as a `mode_t', the mode of the
   created file.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int open (const char *__file, int __oflag, ...) __nonnull ((1));

各类文件打开示例如下:

//打开null文件(位于linux-2.6.39/tools/perf/util/run-command.c的13行)
int fd = open("/dev/null", O_RDWR);
//打开挂载(位于linux-2.6.39/arch/um/os-Linux/mem.c的121行)
fd = open("/proc/mounts", O_RDONLY);
//打开控制台console(位于linux-2.6.39/init/do_mounts.c的392行)
fd = sys_open("/dev/console", O_RDWR, 0);
//打开根文件(位于linux-2.6.39/init/do_mounts_initrd.c的48行)
root_fd = sys_open("/", 0, 0);
//打开cpu信息(位于linux-2.6.39/tools/perf/util/svghelper.c的211行)
file = fopen("/proc/cpuinfo", "r");
//用于打开终端(位于linux-2.6.39/arch/um/os-Linux/tty.c的34行)
fd = open("/dev/ptmx", O_RDWR);
3.1.2 一切皆文件

linux中一切设备皆是文件,这里以输入终端terminal相关操作为例,操作如下:

1、打开终端terminal
ubuntu中打开终端快捷键为ctrl+alt+t,可任意打开若干个终端terminal;

2、查看所有打开的terminal终端
命令为ls /dev/pts,每打开一个terminal,/dev/pts目录下会多一个文件(示例如:0  1  2  3  4  5  ptmx); 

3、查看当前terminal
命令为tty, 结果如:/dev/pts/5

4、输出到指定终端
操作如:echo test >/dev/pts/5  , 那么在终端5上将看到输出test

创建terminal流程:

键盘发出创建终端指令
ptmx创建终端
终端

terminal终端数据交互流程:

终端
stdin
shell
stdout

四、进程

进程是系统资源调度和分配的基本单位,是动态变化的实体,拥有自己的地址空间、执行堆栈、文件描述符等。进程间相互独立,进程间可以通过内核公共缓冲区、信号机制等方式进行通信(IPC)。进程使用非负整数作为每个的进程ID。

4.1 进程创建

除系统初始进程外,进程创建的方式有fork、vfork、clone。

4.1.1 fork

fork用于创建普通进程,子进程复制父进程的资源,但子进程有自己的代码段(task_struct)和pid。内核源码定义如下:

//fork系统调用(位于linux-2.6.39/arch/um/kernel/syscall.c的18行)
long sys_fork(void)
{
	long ret;
	current->thread.forking = 1;
	ret = do_fork(SIGCHLD, UPT_SP(&current->thread.regs.regs),
		      &current->thread.regs, 0, NULL, NULL);
	current->thread.forking = 0;
	return ret;
}

//do_fork方法的实现,具体请查看源码(位于linux-2.6.39/kernel/fork.c的1403行)
long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{...}
4.1.2 vfork

vfork用于资源完全共享的进程创建,即父子进程完全共享父进程资源,没有资源的复制,子进程修改某变量,父进程将读取到子进程修改后的变量。内核源码定义如下:

//fork系统调用(位于linux-2.6.39/arch/um/kernel/syscall.c的29行)
long sys_vfork(void)
{
	long ret;

	current->thread.forking = 1;
	ret = do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD,
		      UPT_SP(&current->thread.regs.regs),
		      &current->thread.regs, 0, NULL, NULL);
	current->thread.forking = 0;
	return ret;
}
4.1.3 clone

fork和vfork都是无参的,fork是完全复制,vfork是完全共享。clone则是有参的,有选择的进行资源复制,clone用于创建轻量级进程。内核源码定义如下:

//clone操作(位于linux-2.6.39/kernel/fork.c的257行)
long
sys_clone(unsigned long clone_flags, unsigned long newsp,
     void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
{
   if (!newsp)
      newsp = regs->sp;
   return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
}
4.2 进程终止

父子进程终止,有两类:一是子进程先于父进程终止;二是父进程先于子进程终止。

4.2.1 子进程先于父进程终止

子进程先终止时,系统内核会保存子进程id、终止状态、cpu使用时间等信息,接着父进程调用wait()或waitpid()方法获取子进程信息并进行善后处理,此时内核会释放终止进程占用的存储及关闭打开的文件等操作。没经过父进程处理的终止进程为僵尸进程。

4.2.2 父进程先与子进程终止

如果父进程先终止,则子进程的父进程将变更为init进程(进程号pid为1)。

五、 并发

linux处理并发的有三种方式:多进程并发、多线程并发及I/O多路复用,其中IO复用在后续单独一节中详细介绍。

5.1 多进程并发

父进程接收用户请求,接着fork新的子进程,用户请求便由子进程处理,父进程接着接收新的用户请求。

用户 父进程 子进程 请求 创建子进程 处理用户请求 接收新的用户请求 用户 父进程 子进程
5.2 多线程并发

多进程并发时,创建子进程会占用大量资源,同时进程间通信也比较复杂。而多线程并发中,进程内所有线程共享内存空间、全局变量等资源,同时线程也有自己私有的线程id、存放局部变量的栈、程序运行的程序计数器和栈指针等信息。多线程开销小、切换方便,但由于共享内存空间,存在多线程安全同步问题。

用户 主线程 新线程 请求 创建新线程 处理用户请求 接收新的用户请求 用户 主线程 新线程

六、I/O模型

I/O模型分为5类:阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O、异步I/O。

6.1 阻塞I/O

用户进程发起读数据的系统调用,在数据复制到内核缓冲区和数据从内核缓冲区复制到用户空间两个过程中,用户进程都是阻塞的。

用户进程 内核 系统调用 用户进程阻塞 数据没准备好 数据没准备好 数据准备好了 数据从内核缓冲 区复制到用户进程 结果返回 用户进程 内核
6.2 非阻塞I/O

非阻塞I/O场景,用户进程在第一阶段不阻塞,不断主动检测内核数据是否准备好,若没准备好,可做其他事情。一旦内核数据准备好,用户进程进入第二阶段,此时阻塞,直到内核将数据从内核缓冲区复制到用户进程内并通知用户进程。

用户进程 内核 系统调用 数据没准备好 返回不可读 用户进程不阻 塞,可做其他事 系统调用 数据没准备好 返回不可读 用户进程不阻 塞,可做其他事 系统调用 数据准备好了 返回可读 用户进程阻塞 数据从内核 读入用户进程 结果返回 用户进程 内核
6.3 I/O复用

IO复用让单进程可以同时处理多个IO,由linux内核监听多个描述符,linux内核监听方式有select、poll、epoll方式,后续会详细介绍。以select监听方式为例,I/O复用示意图如下:

用户进程 内核 select系统调用 用户进程阻塞 检测多个文件中是 否有可读数据数据 有可读文件 返回可读文件 系统调用 用户进程阻塞 数据从内核缓冲 区复制到用户进程 结果返回 用户进程 内核
6.3.1 用户进程的I/O系统调用

I/O复用中的系统调用有select、poll、epoll,这些系统调用位于内核中,触发这些系统调用都会阻塞用户进程。它们的功能都是确定是否可以对打开的一个或多个文件进行读写(是否读写就绪),一旦有文件描述符可读写,便通知程序进行读写。

4.3.2 select

select方式采用轮询多个fd,检测是否存在就绪事件(读或写),在多个平台都支持。调用select函数时,会在该函数上阻塞。特点如下:

  • 受限于单进程可打开(监听)的最大文件数(linux默认为1024);
  • 每次调用都要把fd集合从用户态复制到内核态;
  • 每次调用都要在内核中要遍历传入的所有fd;

select系统调用只有一个select函数,在内核源码中定义如下:

//系统调用select(位于linux-2.6.39/include/linux/syscalls.h的626行)
asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp,
			fd_set __user *exp, struct timeval __user *tvp);

//可打开文件描述符的数量定义(位于linux-2.6.39/include/linux/posix_types.h的25行)
#define __FD_SETSIZE	1024
4.3.3 poll

poll和select类似,也是通过轮询多个fd,检测是否存在就绪事件(读或写)。调用poll函数时进程会阻塞。特点如下:

  • poll没有最大文件数限制(基于链表存储);
  • 每次调用都要把fd集合从用户态复制到内核态;
  • 每次调用都要在内核中要遍历传入的所有fd;

poll系统调用只有一个poll函数,在内核源码中定义如下:

//系统调用poll(位于linux-2.6.39/include/linux/syscalls.h的624行)
asmlinkage long sys_poll(struct pollfd __user *ufds, unsigned int nfds,
				long timeout);
4.3.4 epoll

epoll是在linux2.6加入的,基于事件实现,特点如下:

  • 基于mmap内存映射,将用户空间和内核空间的映射到同块内存;
  • epoll没有最大文件数限制(跟内存有关,上限为可打开的最大文件数,通常很大如1207878,查看命令cat /proc/sys/fs/file-max);
  • 基于事件,当就绪事件发生时,直接调用fd上注册的callback函数;

epoll对文件描述符的操作有两种模式,即LT(level trigger)和ET(edge trigger):

  • LT模式:缺省工作方式,就绪事件发生时,如果不处理,下次仍然会得到内核通知;
  • ET模式:就绪事件发生时,如果不处理,不会再次得到内核通知;

epoll由3个函数构成sys_epoll_create(创建epoll句柄)、sys_epoll_ctl(注册监听事件类型)和sys_epoll_wait(等待是监听事件发生),在内核中的源码如下:

//创建epoll句柄(位于linux-2.6.39/include/linux/syscalls.h的629行)
asmlinkage long sys_epoll_create(int size);
//注册监听事件类型(位于linux-2.6.39/include/linux/syscalls.h的631行)
asmlinkage long sys_epoll_ctl(int epfd, int op, int fd,
				struct epoll_event __user *event);
//等待是监听事件发生(位于linux-2.6.39/include/linux/syscalls.h的633行)
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,
				int maxevents, int timeout);
4.4 信号驱动I/O

信号驱动I/O采用信号处理函数,用户进程执行后即返回,第一阶段不阻塞,当可读时,内核发送信号给用户进程,接着用户进程才阻塞直到数据读取完成。流程如下:

用户进程 内核 系统调用,发出信号 用户进程不阻塞 ,接着做其他事 数据没准备好 数据准备好 发出可读信号 系统调用,读数据 进程阻塞 数据从内核缓冲 区复制到用户进程 结果返回 用户进程 内核
4.5 异步I/O

异步I/O同信号驱动I/O类似,只是在读写完成内核才通知用户进程。流程如下:

用户进程 内核 系统调用,发出信号 用户进程不阻塞 ,接着做其他事 数据没准备好 数据准备好 数据从内核 读入用户进程 结果返回 用户进程 内核

七、扩展

7.1 jdk中I/O

jdk中的IO分为三类,jdk1.4版本前为BIO(阻塞IO),之后为NIO(同步非阻塞IO),jdk1.7版本推出NIO2(AIO,异步非阻塞IO),具体请查看相关内容。

7.2 查看管理系统资源
7.2.1 查看及暂时修改资源限制

ulimit用于控制shell执行程序的资源,注意使用ulimit修改资源值时,只能临时修改,退出shell后不再生效(永久修改需修改配置文件/etc/security/limits.conf)。

命令格式为:ulimit [option]
参数:
-a:查看当前资源的所有限制;
-H:设置资源硬性限制;
-S:设置资源软(弹性)限制;
-c:core文件的最大值,单位为区块;
-d:程序数据节区的最大值,单位KB;
-f:shell所能建立的最大值,单位区块;
-n:同时可打开的最大文件数;
-p:管道缓冲区大小,单位512字节;
-s:堆的最大值,单位KB;
-t:cpu使用时间最大值,单位秒;
-u:用户可以打开的最大程序数;
-v:虚拟内存的最大值,单位KB;

使用示例:

#查看资源所有限制
命令:ulimit -a
输出:
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 47348
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 47348
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

#单独查看文件最大数
命令:ulimit -n
输出:1024

#修改文件最大数
命令:ulimit -SHn 2048
结果:文件数变为2048,使用ulimit -n查看
7.2.2 进程级别资源配置

永久修改进程级别资源限制需要修改配置文件(文件内有使用详细说明)

#修改配置,永久生效,不用重启服务器,重新登录shell后再重启程序即可,如修改单进程可打开最大文件数
#添加配置:  *               hard    nofile             10000
vim /etc/security/limits.conf
7.3 系统级别最大文件数
#查看可打开最大文件数,如:1207878
cat /proc/sys/fs/file-max

#永久修改可打开最大文件数,添加类似配置:fs.file-max=2000000,接着执行sysctl -p命令使配置生效
vim /etc/sysctil.conf

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值