Linux System Programming 学习笔记(四) 高级I/O

1. Scatter/Gather I/O

a single system call  to  read or write data between single data
stream and multiple buffers

This type of I/O is so named because the data is scattered into or gathered
from the given vector of buffers

Scatter/Gather I/O 相比于 C标准I/O有如下优势:

(1)更自然的编码模式

如果数据是自然分隔的(预先定义的结构体字段),那么 Scatter/Gather I/O  更直观

(2)效率高

A single vectored I/O operation can replace multiple linear I/O
operations

(3)性能好

减少了系统调用数

(4)原子性

Scatter/Gather I/O 是原子的,而
multiple linear I/O operations 则有多个进程交替I/O的风险

#include <sys/uio.h>

struct iovec {
void *iov_base; /* pointer to start of buffer */
size_t iov_len; /* size of buffer in bytes */
};

Both functions always operate on the segments in order, starting with
iov[0], then iov[1], and so on, through iov[count-1].

/* The readv() function reads count segments from the file descriptor fd into the buffers described by iov */
ssize_t readv (int fd, const struct iovec *iov, int count);

/* The writev() function writes at most count segments from the buffers described by iov into the file descriptor fd */
ssize_t writev (int fd, const struct iovec *iov, int count);

注意:在Scatter/Gather
I/O操作过程中,内核必须分配内部数据结构来表示每个buffer分段,正常情况下,是根据分段数count进行动态内存分配的,

但是当分段数count较小时(一般<=8),内核直接在内核栈上分配,这显然比在堆中动态分配要快


#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/uio.h>

int main(int argc, char* argv[])
{
struct iovec iov[3];
char* buf[] = {
"The term buccaneer comes from the word boucan.\n",
"A boucan is a wooden frame used for cooking meat.\n",
"Buccaneer is the West Indies name for a pirate.\n" };

int fd = open("wel.txt", O_WRONLY | O_CREAT | O_TRUNC);
if (fd == -1) {
fprintf(stderr, "open error\n");
return 1;
}

/* fill out three iovec structures */
for (int i = 0; i < 3; ++i) {
iov[i].iov_base = buf[i];
iov[i].iov_len = strlen(buf[i]) + 1;
}

/* with a single call, write them out all */
ssize_t nwrite = writev(fd, iov, 3);
if (nwrite == -1) {
fprintf(stderr, "writev error\n");
return 1;
}
fprintf(stdout, "wrote %d bytes\n", nwrite);
if (close(fd)) {
fprintf(stdout, "close error\n");
return 1;
}

return 0;
}


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <sys/stat.h>

int main(int argc, char* argv[])
{
char foo[48], bar[51], baz[49];
struct iovec iov[3];
int fd = open("wel.txt", O_RDONLY);
if (fd == -1) {
fprintf(stderr, "open error\n");
return 1;
}

/* set up our iovec structures */
iov[0].iov_base = foo;
iov[0].iov_len = sizeof(foo);
iov[1].iov_base = bar;
iov[1].iov_len = sizeof(bar);
iov[2].iov_base = baz;
iov[2].iov_len = sizeof(baz);

/* read into the structures with a single call */
ssize_t nread = readv(fd, iov, 3);
if (nread == -1) {
fprintf(stderr, "readv error\n");
return 1;
}

for (int i = 0; i < 3; ++i) {
fprintf(stdout, "%d: %s", i, (char*)iov[i].iov_base);
}
if (close(fd)) {
fprintf(stderr, "close error\n");
return 1;
}

return 0;
}

writev的简单实现:


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

ssize_t my_writev(int fd, const struct iovec* iov, int count)
{
ssize_t ret = 0;
for (int i = 0; i < count; ++i) {
ssize_t nr = write(fd, iov[i].iov_base, iov[i].iov_len);
if (nr == -1) {
if (errno == EINTR)
continue;
ret -= 1;
break;
}
ret += nr;
}
return nr;
}

In fact, all I/O inside the Linux kernel is vectored; read() and write() are implemented as vectored
I/O with a vector of only one segment

2. epoll

poll 和 select 每次调用都需要监测整个文件描述符集,当描述符集增大时,每次调用对描述符集全局扫描就成了性能瓶颈

(1) creating a new epoll instance

/* A successful call to epoll_create1() instantiates a new epoll instance and returns a file descriptor associated with the instance */
#include <sys/epoll.h>
int epoll_create(int size);

parameter size used to provide a  hint about the number of file
descriptors to be watched;

nowadays the kernel dynamically
sizes the required data structures and this parameter just needs to be
greater than zero

(2) controling epoll


/* The epoll_ctl() system call can be used to add file descriptors to and remove file descriptors from a given epoll context */
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

struct epoll_event {
__u32 events; /* events */
union {
void* ptr;
int fd;
__u32 u32;
__u64 u64;
} data;
};

a. op parameter

EPOLL_CTL_ADD   // Add a monitor on the file associated with the file descriptor fd to the epoll instance associated with epfd
EPOLL_CTL_DEL // Remove a monitor on the file associated with the file descriptor fd from the epoll instance associated with epfd
EPOLL_CTL_MOD // Modify an existing monitor of fd with the updated events specified by event

b. event parameter

EPOLLET     // Enables edge-triggered behavior for the monitor of the file ,The default behavior is level-triggered
EPOLLIN // The file is available to be read from without blocking
EPOLLOUT // The file is available to be written to without blocking

对于结构体struct epoll_event 里的data成员,通常做法是将data联合体里的fd设置为第二个参数fd,即
event.data.fd = fd

To add a new watch on the file associated with fd to the epoll
instance  epfd :


#include <sys/epoll.h>

struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLOUT

int ret = epll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
if (ret) {
fprintf(stderr, "epll_ctl error\n");
}

To modify an existing event on the file associated with fd on the epoll
instance epfd :


#include <sys/epoll.h>

struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;

int ret = epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &event);
if (ret) {
fprintf(stderr, "epoll_ctl error\n");
}

To remove an existing event on the file associated with fd from the
epoll instance epfd :


#include <sys/epoll.h>

struct epoll_event event;

int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
if (ret) {
fprintf(stderr, "epoll_ctl error\n");
}

(3) waiting for events with epoll

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

The return value is the number of events, or ?1 on error


#include <sys/epoll.h>

#define MAX_EVENTS 64

struct epoll_event* events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);
if (events == NULL) {
fprintf(stdout, "malloc error\n");
return 1;
}

int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nready < 0) {
fprintf(stderr, "epoll_wait error\n");
free(events);
return 1;
}

for (int i = 0; i < nready; ++i) {
fprintf(stdout, "event=%ld on fd=%d\n", events[i].events, events[i].data.fd);
/* we now can operate on events[i].data.fd without blocking */
}
free(events);

(4) Level-Triggered  vs. Edge-Triggered

考虑一个生产者和一个消费者通过Unix 管道通信:

a. 生产者向管道写 1KB数据

b. 消费者在管道上执行 epoll_wait, 等待管道中数据可读

如果是 水平触发,epoll_wait 调用会立即返回,表明管道已经 读操作就绪

如果是 边缘触发,epoll_wait 调用不会立即返回,直到 步骤a
发生,也就是说,即使管道在eoll_wait调用的时候是可读的,该调用也不会立即返回直至数据已经写到管道。

select和epoll的默认工作方式是 水平触发,边缘触发通常是应用于 non-blocking I/O,
并且必须小心检查EAGAIN

3. Mapping Files into Memory

/* A call to mmap() asks the kernel to map len bytes of the object represented by the file  descriptor fd, 
starting at offset bytes into the file, into memory
*/
#include <sys/mman.h>
void* mmap(void* addr, size_t len, int prot, int flags, int fd, off_t offset);

void* ptr = mmap(0, len, PROT_READ, MAP_SHARED, fd, 0);

参数addr和offset必须是内存页大小的整数倍

如果文件大小不是内存页大小的整数倍,那么就存在填补为0的空洞,读这段区域将返回0,写这段区域将不会影响到被映射的文件内容

Because addr and offset are usually
0, this requirement is not overly difficult to meet

int munmap (void *addr, size_t len);

munmap() removes any mappings that contain pages located anywhere in
the process address space starting at addr,

which must be page-aligned, and continuing for len bytes


#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
if (argc < 2) {
fprintf(stderr, "usage:%s <file>\n", argv[0]);
return 1;
}

int fd = open(argv[1], O_RDONLY);
if (fd == -1) {
fprintf(stderr, "open error\n");
return 1;
}

struct stat sbuf;
if (fsat(fd, &sbuf) == -1) {
fprintf(stderr, "fstat error\n");
return 1;
}

if (!S_ISREG(sbuf.st_mode)) {
fprintf(stderr, "%s is not a file\n", argv[1]);
return 1;
}
void* ptr = mmap(0, sbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
fprintf(stderr, "mmap error\n");
return 1;
}

if (close(fd)) {
fprintf(stderr, "close error\n");
return 1;
}

for (int i = 0; i < sbuf.st_size; ++i) {
fputc(ptr[i], stdout);
}

if (munmap(ptr, sbuf.st_size) == -1) {
fprintf(stderr, "munmap error\n");
return 1;
}
return 0;
}

mmap的优点:

(1) 读写内存映射文件 可以避免 read 和 write
系统调用发生的拷贝

(2) 除了潜在的内存页错误,读写内存映射文件不会有 read 和 write 系统调用时的上下文切换开销

(3) 当多个进程映射相同内存时,内存数据可以在多个进程之间共享

(4) 内存映射定位时只需要平常的指针操作,而不需要 lseek 系统调用

mmap的缺点:

(1) 内存映射必须是页大小的整数倍,通常在 调用返回的内存页 和
被映射的实际文件之间存在 剩余空间,例如内存页大小是4KB,那么映射7Byte文件将浪费4089Byte内存

(2) 内存映射必须适合进程的地址空间,32位地址空间,如果有大量的内存映射,将产生碎片,将很难寻找到连续的大区域

(3) 创建和维护 内存映射和内核相关数据结构 有一定开销

文件和映射内存之间的同步:

#include <sys/mman.h>
int msync (void *addr, size_t len, int flags);

msync() flushes back to disk any changes made to a file mapped via mmap(),
synchronizing the mapped file with the mapping

Without invocation of msync(), there is no guarantee that a dirty mapping
will be written back to disk until the file is unmapped

4. 同步 异步

5. I/O调度和I/O性能

a single disk seek can average over 8 milliseconds,25 million times
 longer than a single processor cycle!

I/O schedulers perform two basic operations: merging
and sorting

Merging is the process of taking two or more adjacent I/O requests and
combining them into a single request

假设一个请求读磁盘块5,另一个请求读磁盘块6和7,调度器可以合并成单一请求读磁盘块5到7,这将减少一半的I/O操作数

Sorting is the process of arranging pending I/O requests in ascending block
order (尽可能保证顺序读写,而不是随机读写)

If an I/O scheduler always sorted new requests by the order of insertion,it
would be possible to starve requests to

far-off blocks indefinitely (I/O调度算法,避免饥饿现象)

使用电梯调度算法避免某个I/O请求长时间得不到响应

Linus Elevator I/O scheduler

The Deadline I/O Scheduler

The Anticipatory I/O Scheduler

The CFQ I/O Scheduler

The Noop I/O Scheduler

Sorting by inode number is the most commonly used method for
scheduling I/O requests in user space

Linux System Programming 学习笔记(四) 高级I/O

时间: 2024-08-18 11:33:43

Linux System Programming 学习笔记(四) 高级I/O的相关文章

Linux System Programming 学习笔记(八) 文件和目录管理

1. 文件和元数据 每个文件都是通过inode引用,每个inode索引节点都具有文件系统中唯一的inode number 一个inode索引节点是存储在Linux文件系统的磁盘介质上的物理对象,也是LInux内核通过数据结构表示的实体 inode存储相关联文件的元数据 ls -i 命令获取文件的inode number /* obtaining the metadata of a file */ #include <sys/types.h> #include <sys/stat.h>

Linux System Programming 学习笔记(十) 信号

1. 信号是软中断,提供处理异步事件的机制 异步事件可以是来源于系统外部(例如用户输入Ctrl-C)也可以来源于系统内(例如除0) 内核使用以下三种方法之一来处理信号: (1) 忽略该信号.SIGKILL和SIGSTOP不能被忽略. (2) 捕捉并且处理该信号.The kernel will suspend execution of the process's current code path and jump to a previously registered function. SIGK

Linux System Programming 学习笔记(五) 进程管理

1. 进程是unix系统中两个最重要的基础抽象之一(另一个是文件) A process is a running program A thread is the unit of activity inside of a process the virtualization of memory is associated with the process, the threads all share the same memory address space 2. pid The idle pro

Linux System Programming 学习笔记(九) 内存管理

1. 进程地址空间 Linux中,进程并不是直接操作物理内存地址,而是每个进程关联一个虚拟地址空间 内存页是memory management unit (MMU) 可以管理的最小地址单元 机器的体系结构决定了内存页大小,32位系统通常是 4KB, 64位系统通常是 8KB 内存页分为 valid or invalid: A valid page is associated with an actual page of data,例如RAM或者磁盘上的文件 An invalid page is

Linux System Programming 学习笔记(七) 线程

1. Threading is the creation and management of multiple units of execution within a single process 二进制文件是驻留在存储介质上,已被编译成操作系统可以使用,准备执行但没有正运行的休眠程序 进程是操作系统对 正在执行中的二进制文件的抽象:已加载的二进制.虚拟内存.内核资源 线程是进程内的执行单元 processes are running binaries, threads are the smal

Linux System Programming 学习笔记(十一) 时间

1. 内核提供三种不同的方式来记录时间: Wall time (or real time):actual time and date in the real world Process time:the time that a process spends executing on a processor 包括用户时间user time 和 系统时间system time Monotonic time:use the system's uptime (time since boot) for t

Linux System Programming 学习笔记(六) 进程调度

1. 进程调度 the process scheduler is the component of a kernel that selects which process to run next. 进程调度器需要使 处理器使用率最大化,并且提供 使多个进程并发执行的虚拟 Deciding which processes run, when, and for how long is the process scheduler's fundamental responsibility. 时间片:th

Linux System Programming 学习笔记(二) 文件I/O

1.每个Linux进程都有一个最大打开文件数,默认情况下,最大值是1024 文件描述符不仅可以引用普通文件,也可以引用套接字socket,目录,管道(everything is a file) 默认情况下,子进程会获得其父进程文件表的完整拷贝 2.打开文件 open系统调用必须包含 O_RDONLY,O_WRONLY,O_RDWR 三种存取模式之一 注意 O_NONBLOCK模式 int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644

Linux System Programming 学习笔记(一) 介绍

1. Linux系统编程的三大基石:系统调用.C语言库.C编译器 系统调用:内核向用户级程序提供服务的唯一接口.在i386中,用户级程序执行软件中断指令 INT n 之后切换至内核空间 用户程序通过寄存器告知内核执行系统调用的所需参数 2. API 和 ABI API:application programming interface 定义源码级接口,必须确保源码级兼容 ABI:application binary interface 定义二进制接口,必须确保二进制兼容 ABI包括:调用约定.字