Linux学习 --系统IO

发布时间 2023-12-03 15:48:50作者: (喜欢黑夜的孩子)

备注一些学习过程中的笔记......................

 

*****************************************************************************

Linux对文件进行读写之前需要打开文件

 

Linux中,遵循一切皆文件,只要是能读写的东西都能用文件描述符来访问。如文件描述符不仅仅可以用在普通文件访问,还能够访问设备文件,管道,目录,FIFO,套接字等

 

进程和打开文件之间的关系

内核为每个进程维护一个打开文件的列表,称为文件表由文件描述符进行索引,列表中每一项都包含一个打开文件的信息。打开一个文件返回一个文件描述符。

 

每一个进程惯例打开3个文件描述符

文件描述符0标准输入

文件描述符1标准输出

文件描述符2标准错误

 

父子进程的文件列表拷贝

子进程默认会得到一份父进程的文件表拷贝,对子进程的操作也不会影响到父进程的文件表。

当然也可以让子进程共享父进程的文件表

 

打开文件

 

 

打开一个文件的方法可以通过open()creat()调用打开

 

 

 

包含的头文件


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
API 
int open (const char *name, int flags);
int open (const char *name, int flags, mode_t
mode);

 

函数的返回值-返回一个文件描述符

 

参数介绍

Name:要打开的路径名中的文件名

Flags:

O_RDONLY,O_WRONLY,O_ RDWR三个中选一个分别代表只读,只写,可读可写打开文件。

Flags也能够进行按位与操作同时调用多个参数

 

 

 

 

Mode提供创建的文件的权限,除非是创建新文件对文件权限有要求,否则mode参数会被忽略。当文件创建时(调用了O_CREAT),mode提供新建文件的权限。Mode参数是常见的Unix权限位集合 0644:所有者可读写,其他人只可读)(0777:所有人可读可写)

 

 

Mode可以利用的参数

 

 

 实例

 以只读的方式打开一个文件并返回文件描述符

 

文件所属

新文件所有者:文件所有者id就是创建该文件的进程的有效用户id

 

 creat()函数

O WRONLY | O CREAT | O TRUNC的组合经常在open函数中使用,因此专门给它设计了一个creat()函数

头文件
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h>

API 
int creat (const char *name, mode_t mode);
(实际上该函数就是对open函数的特殊封装)

 

典型使用案例

 

等同于

 

返回值和错误码

Opencreat()都会返回一个文件描述符,错误时返回-1,并将errno设置一个合适的错误值

 

 

 

 

Read读取文件

 

头文件
#include <unistd.h>

API 
ssize_t read (int fd, void *buf, size_t len);

该函数返回由fd指向的文件当前偏移量读取len个字节到buf,如果读取成功返回写入到buf的字节数,出错返回-1并设置errno。且每次读取之后打开文件的偏移量都会增加之前的读取字节数

返回值

返回值=len:正常读取

返回值<len:读取被中断/可被读取的字节数少于len

返回值=0:read偏移量已经到达了文件结尾EOF(End of File)(非阻塞情况下)

阻塞情况下会一直等待有效数据

返回值=-1read()被错误中断,如被信号打断会返回-1并设置errno

 

 考虑所有read返回的结果

ssize_t ret;    //读取结果的返回值
while (len != 0 && (ret = read(fd, buf, len)) !=0)
//当要读取的len!=0且文件偏移没达到EOF时可读取文件
{
if (len > SSIZE_MAX)    //len是有上限的,它不能超过0x7fffffff
 
len = SSIZE_MAX;

    //读取出错返回-1
    if (ret == -1) {
        //错误原因确认,如果为EINTR(被信号中断)申请重新开始读取
        if (errno == EINTR)
            continue;
if (errno == EAGAIN)     //在非阻塞模式下如果没有读到数据时返回该值
/* resubmit later */

        //打印错误原因
        perror(”read”);
        break;
    }
    //读取成功修改len值
    len -= ret;    //len减去已经读取的字节数
    buf += ret; //移动buf指针
}

 

非阻塞读

当程序员希望没数据读时直接返回的情况称为非阻塞I/O,可以实现在操作多个文件时避免丢失其它文件的可用数据

 

 

 write写文件

 

头文件
#include <unistd.h>

API 
ssize_t write (int fd, const void *buf, size_t 
count);

 

bufcount个字节数据写入fd指向的文件中。不支持定位的文件则从头开始写入

 

 

成功时返回写入字节数,并更新文件位置。错误返回-1

使用实例

 

 

 

unsigned long word = 1720;    //要写入的结构
size_t count;            
ssize_t nr;                //保存返回值
count = sizeof(word);    //要写入的字节数
nr = write(fd, &word, count);
if (nr == -1)            //写入失败
/* error, check errno */
else if (nr != count)    //写入量不是要求的字节数
/* possible error, but ’errno’ not set */


/*通过循环部分写入确保要写入的数据都写入成功,同read*/
ssize_t ret, nr; 
while (len != 0 && (ret = write (fd, buf, len)) 
!= 0) { 
if (ret == -1) { 
if (errno == EINTR) 
continue; 
perror (”write”); 
break; 
} 
len -= ret; 
buf += ret; 
}

 

追加模式写

Fd在追加模式下打开时(指定打开方式为 O_APPEND参数),写操作从文件描述符的文件末尾开始。这样可以避免多个文件同时写入时造成冲突,尤其适合用来写日志文件......

 

 

非阻塞写

非阻塞模式下打开(设置O_NONBLOCK参数),无数据可写入时不会阻塞而是返回-1并设置errnoEAGAIN

 

关于write的写入行为

 

Write的写入是分为两个阶段

 

第一个阶段write返回后,写入内容会被拷贝到一个缓冲区

 

第二个阶段内核处理缓冲区数据再写入磁盘中(回写)过程

 

会不会影响read..

 

不会。Read操作也会直接从缓冲区中读取需要的数据,即从缓冲区响应read的请求

 

 

 

内核设计了一个缓存最大时效机制,所有缓存数据都会在超过时效之前写入磁盘,该值(最大时效值)同样可以被配置。

 

当然,想做到写同步也是可能的,这需要同步IO的知识

 

 

同步I/O

 

作用:

确保数据写入磁盘

 

包含头文件
#include <unistd.h>
API 
int fsync (int fd);
int fdatasync (int fd);    

返回值和错误码

成功时都返回0,失败返回-1并设置errno

 

当调用fsync时可以保证缓存区的脏数据立刻被写入磁盘中fd必须是可写方式打开,在确保数据已经全部写入前该函数不会返回。

 

 

当调用fdatasync 仅仅写入数据,不保证元数据同步到磁盘

errno可能出现的值

 

 关于sync

Sync()
对磁盘中所有缓冲区进行同步

包含的头文件
#include <unistd.h>
API 
void sync (void);

总是成功返回并确保数据和元数据(数据的相关联信息,如数据的组织,数据域等)都能写入磁盘

 

 

打开文件过程中的O_SYNC标志

 

 

作用:在OPEN中使用,使所有文件上的I/O操作同步,

该标志作用其实就是强制让写操作同步,保证write(的I/O同步)

 

 

 

 

 

 

打开文件过程中的O_DSYNC O_RSYNC

POSIX定义的另外两个文件IO同步标志

O_DSYNC普通数据同步,元数据不同步(类似隐式调用fdatasync

O_RSYNC:  要求读请求同写请求一样同步,因此必须和O_SYNC/O_DSYNC一起使用

 

 关于直接IO

 

Open中使用O_DIRECT标志,使内核最小化I/O管理影响,使用后I/O操作会忽略页缓存机制,直接操作用户空间缓冲区和设备初始化,所有IO都是同步的

 

 

关闭文件

包含头文件
#include <unistd.h>

API
int close (int fd); 

调用成功返回0,否则-1并设置errno

作用:

Close解除文件和文件描述符关系,以及文件和进程的关系,并释放和该文件有关的数据结构

要保证关闭文件前,数据写入磁盘 ------必须使用同步I/O

返回错误码

EBADF:给定的文件描述符不合法

EIO:和close不相干的底层I/O错误?

 

 lseek()查找

 

头文件包含
#include <sys/types.h> 
#include <unistd.h>

API 
off_t lseek (int fd, off_t pos, int origin); 

调用成功返回新文件位置,失败返回-1,并设置errno

 

作用:

    更改文件的操作位置

参数说明:

 实例

显然可以利用lseek()实现定位到当前文件的开始/末尾

 

文件末尾之后的查找

当对该位置进行读操作后返回EOF

当对该位置进行写操作后会在前后字节之间的空挡补0

这种零填充的方法称空洞,空洞不会占用物理磁盘空间,这说明文件系统所有文件大小可能超过磁盘物理大小

 

错误码

 

定位读写

Linux提供了writeread的变体来替代lseek()

包含的头文件
#include <unistd.h>

API 
ssize_t pread (int fd, void *buf, size_t count, 
off_t pos);
从文件描述符fd的pos位置读取count个字节到buf中


包含的头文件
#include <unistd.h>
API 
ssize_t pwrite (int fd, const void *buf, size_t 
count, off_t pos); 
从buf中写入count个字节到fd文件描述符指定的文件中中

 

Pread返回0表示EOFpwrite表面没有写任何东西

二者返回-1并设置errno,lseek()和read()write()的错误码都可能出现

 

注意:

Preadpwrite调用完后不会更改文件的位置,如果中间混杂了普通的read/write可能会破坏定位读写

优势

使用pwritepread相当于在writeread之前调用了lseek()进行定位。但存在差异

  1. 更加方便使用
  2. 使用完后不修改文件位置指针
  3. 避免了线程竞争问题

 

截断文件

包含头文件
#include <unistd.h> 
#include <sys/types.h>
API 
int ftruncate (int fd, off_t len); 
int truncate (const char *path, off_t len);
成功返回0,失败返回-1并设置错误码

这两个函数都能将文件截短到len指定的长度。

前者的fd指向一个打开的可写的文件描述符fd

后者是路径上的一个可写文件

#include <unistd.h> 
#include <stdio.h> 
int main( ) 
{ 
int ret;

ret = truncate (”./pirate.txt”, 45); 
if (ret == -1) { 
perror (”truncate”); 
return -1; 
} 
return 0; 
} 

 

I/O多路复用

 

解决应用程序需要在多个文件描述符之间阻塞的问题。(一般只能阻塞在一个IO口上)

为了让文件描述符能够在可读写时解除阻塞,同时不能读写时进入睡眠,出现了I/O多路复用

 

 

I/O复用遵循原则

 

  1. 任何文件描述符准备好时通知
  2. 文件描述符就绪前处于睡眠状态
  3. 唤醒:唤醒准备好的文件描述符
  4. 不阻塞的情况下解决所有就绪I/O文件描述符

 

 

Linux提供了三种I/O多路复用方案selectpoll,epoll

包含头文件
#include <sys/time.h> 
#include <sys/types.h> 
#include <unistd.h>

相关API 
int select (int n, 
fd_set *readfds, 
fd_set *writefds, 
fd_set *exceptfds, 
struct timeval *timeout); 
FD_CLR(int fd, fd_set *set); 
FD_ISSET(int fd, fd_set *set); 
FD_SET(int fd, fd_set *set); 
FD_ZERO(fd_set *set);

 

#include <sys/time.h> 
struct timeval { 
long tv_sec; /* seconds */ 
long tv_usec; /* microseconds */ 
};

 

关于fd_set结构体

其实偏向于一个文件描述符数组,能够保存需要管理的文件描述符

 

 

typedef struct fd_set {
    unsigned long fds_bits[FD_SETSIZE / sizeof(unsigned long)];
} fd_set;

 

 

 

关于select()

实现多个文件描述符的同时阻塞,在指定的时间内没有文件描述符准备好I/O操作/超过了时间限制就会阻塞

 

参数介绍

Readfds :确认是否有可读数据

Writefds :确认是否有可写数据

Exceptfds :确认是否有异常发生/带外数据

N :所有调用的文件描述符中的最大值+1

Timeval :可以超过的时间限制

 

 

对上面三个文件描述符集的操作是通过宏来操作而不是直接操作

 

//从指定集合中移除所有文件描述符
FD_ZERO(&writefds); 
//向指定集合中添加一个文件描述符
FD_SET(fd, &writefds);
//从指定集合中移除一个文件描述符
FD_CLR(fd, &writefds);

//确认某个文件描述符是否还在集合中
if (FD_ISSET(fd, &readfds)) 

如果还在则返回非零值,说明I/O已经就绪可读写

如果返回零说明不在集合中,I/O还没准备好读写

 

Select实现微秒级延迟

struct timeval tv; 
tv.tv_sec = 0; 
tv.tv_usec = 500;        //微秒 
/* sleep for 500 microseconds */ 
select (0, NULL, NULL, NULL, &tv); 

关于pselect()

包含头文件
#define _XOPEN_SOURCE 600 
#include <sys/select.h>

API 
int pselect (int n, 
fd_set *readfds, 
fd_set *writefds, 
fd_set *exceptfds, 
const struct timespec *timeout, 
const sigset_t *sigmask); 
FD_CLR(int fd, fd_set *set); 
FD_ISSET(int fd, fd_set *set); 
FD_SET(int fd, fd_set *set); 
FD_ZERO(fd_set *set); 

基本上和select一致,但存在不同点

使用的超时结构体不同,增加了纳秒级别的计时

struct timespec { 
long tv_sec; /* seconds */ 
long tv_nsec; /* nanoseconds */ 
};

sigmask = NULL时,等同于select()

增加pselect是为了增加sigmask参数解决信号和等待文件描述符之间的竞争条件

 

poll系统调用

Poll()的系统调用是System V IO多路复用方案

 

头文件
#include <sys/poll.h>
API 
int poll (struct pollfd *fds, unsigned int nfds, 
int timeout);

参数

Fds:文件描述符结构体

Nfds:结构体个数

Timeout超时时间

pollfd结构体:

头文件
#include <sys/poll.h> 
struct pollfd { 
int fd; /* file descriptor */ 
short events; /* requested events to watch */ 
short revents; /* returned events witnessed */ 
};
当事件不再成立时,events的事件会被转移到revents中

要监视的事件的位掩码

poll的使用案#include <unistd.h>#include <sys/poll.h>#define TIMEOUT 5 /* poll timeout, in seconds */int main (void)

{
//两个事件结构体
struct pollfd fds[2]; int ret; /* watch stdin for input */

//
STDIN_FILENO表示的是标准输入描述符,这里表示监视标准输入描述符是否可读(用户是否写入数据)的事件
fds[0].fd = STDIN_FILENO; 
fds[0].events = POLLIN;
/* watch stdout for ability to write (almost always true) */


//STDOUT_FILENO表示的是标准输出描述符,这里表示监视标准输出描述符是否写阻塞(有没有数据可以写入这个文件描述符)的事件
fds[1].fd = STDOUT_FILENO;
fds[1].events = POLLOUT;
/* All set, block! */
//执行poll
ret = poll (fds, 2, TIMEOUT * 1000);
if (ret == -1) { perror (”poll”); return 1; }
if (!ret) { printf (”%d seconds elapsed.\n”, TIMEOUT); return 0; }
//返回0代表执行成功,其它都出现错误
if (fds[0].revents & POLLIN)  // revents中出现POLLIN,说明事件成立(用户写入了数据)
    printf (”stdin is readable\n”);
  if (fds[1].revents & POLLOUT) // revents中出现POLLOUT,说明事件成立(可以将数据写入标准输出中)
    printf (”stdout is writable\n”);
return 0;
}
 

 

 

 

 

ppoll()
头文件
#define _GNU_SOURCE 
#include <sys/poll.h>
API 
int ppoll 
(struct pollfd *fds, 
nfds_t nfds, 
const struct timespec *timeout, 
const sigset_t *sigmask);

poll select哪个好用?

poll系统调用优先级>select

使用poll不用去计算文件描述符的最大值

相比较select需要确定文件描述符最大值,poll只需要定义数组就够了

Select返回后再次进入需重新初始化,poll不需要

但是

Select的移植性更好,部分Unix系统不支持poll()

select提供微秒级定时服务,poll提供的纳秒级其实是不精确的

 

内核内幕

 

Linux如何实现IO

-虚拟文件系统(VFS),页缓存,页回写,IO调度器

 

虚拟文件系统

 

作用:允许内核在不知道文件系统类型的情况下,使用文件系统函数和操作文件系统数据

 

实现原理:使用一种通用文件模型,它是所有Linux文件系统的基础。它要求所有的文件系统之间需要有共性

 

举个例子来说,当在用户空间的某应用调用了read()系统调用,在该进程进入内核态时将系统调用转交给处理器处理,最后交给read()系统调用。内核确定read()的文件描述符对应的对象类型后调用相应类型的read()函数。该类型的read()函数执行完后执行结果返回给用户空间的read()调用。(即将数据复制返回给用户空间的系统调用处理器,然后复制数据到用户空间),最后用户空间的read()系统调用返回使进程继续执行

 

 

页缓存

 

页缓存是一种在内存中保存最近磁盘文件系统访问过的数据的方式(访问硬盘的速度要远远慢于访问内存)-----------空间换时间

 内核寻找数据的第一站

页缓存是内核寻找文件系统数据的第一站,只有在页缓存中找不到需要的数据才会调用存储子系统从磁盘中读取数据。当某一数据被读取时,就会从磁盘中读入页缓存,后续再从缓存中读出去

 

 

页缓存动态进行,随着越来越多数据进入缓存,缓存区会增大,但存在最大空间。

当不能再增大缓存区时,释放掉最少使用到的页来腾出空间存储硬盘读取的数据,或者和硬盘交换一块很少使用的数据。使用哪种方式,Linux内核使用了平衡交换数据和清理页缓存的启发式方法。可以通过配置一个值来改变倾向,值偏高时倾向于页缓存保留(硬盘交换数据),较低时偏向于清理缓存

 

 

页缓存预读机制 -每次读请求时会从磁盘数据中读取更多的数据到内存中(减少读取磁盘的次数),当内核读取磁盘一块数据时,也会读取其后1/2块数据。内核可以在进程操作第一块读取的内存数据时完成预读,等进程对下一块硬盘数据发出请求时直接将预读数据交过去而不需要再进行I/O操作

 

内核管理预读 -当某个进程持续使用预读来的数据,内核会增加预读窗口,预读更多的数据。

 

页回写

向硬盘中写入数据利用了缓存区机制(具体参考write()函数),进程发出写请求时,写入的数据会被拷贝到一片缓存区中并标记该缓存区为‘脏’内存,当对同一片缓存区进入二次写操作时也只是对缓存区数据进行更新。

 

最终要将‘脏’数据写入硬盘实现内存和硬盘的数据同步,这一过程称为“回写”

触发回写机制

  1. 缓存区空闲内存小于某个值时,回写到硬盘中
  2. 脏的缓存区寿命超过设定的阈值时间时回写到硬盘中

 

回写由内核线程pdflush操作,以上两个条件会触发该线程执行