UNIX环境编程-高级IO(12)
视频教程
代码链接
一、有限状态机
1.概念
有限状态机(Finite State Machine, FSM)是用于表示和控制执行序列的数学模型。它可以处于有限数量的状态之一,而且在任意时刻只能处于其中的一个状态。FSM的状态、状态转移和可能的输入动作都是预定义的。
FSM通常分为两类:
- 确定性有限自动机(DFA):对于任何给定的输入和当前状态,其转移是唯一确定的。
- 非确定性有限自动机(NFA):对于给定的输入和当前状态,可能有多个可能的下一个状态
有限状态机可以使用以下几个元素来描述:
- 状态 (States):FSM可以处于的所有可能情况。
- 初始状态 (Start State):FSM开始操作时的状态。
- 输入 (Inputs):可以影响状态转移的外部决策。
- 转移函数 (Transition Function):定义了基于当前状态和输入如何从一个状态转移到另一个状态。
- 结束状态 (End States or Accept States):在某些类型的FSM中定义了特定的结束或接受状态
2.实例(数据中继)
(1)任务要求

(2)补充知识
fcntl()
通过这个函数,你可以更改已经打开的文件的属性
1 2 3
| #include <fcntl.h>
int fcntl(int fd, int cmd, ... );
|
其中:
fd
是要操作的文件描述符。
cmd
是一个指定操作的命令。
arg
是可选参数,其需求取决于 cmd
。
常用的 cmd
命令和对应的操作包括:
F_DUPFD
: 复制文件描述符。
F_GETFD
: 获取文件描述符标志。
F_SETFD
: 设置文件描述符标志。
F_GETFL
: 获取文件状态标志。
F_SETFL
: 设置文件状态标志。
F_GETLK
, F_SETLK
, F_SETLKW
: 用于文件锁定
EAGAIN
EAGAIN
(或在某些系统上是 EWOULDBLOCK
) 是一个在POSIX兼容的操作系统中定义的错误码,通常与非阻塞I/O操作相关
当你对一个非阻塞的文件描述符执行I/O操作(如read()
或write()
),而该操作不能立即完成时,系统通常不会将调用线程阻塞,而是立即返回这个错误码(假错误)
这里有一些常见的例子,说明什么时候可能会遇到 EAGAIN
:
- 非阻塞读操作:如果你尝试从一个空的非阻塞套接字或其他设备读取数据,而没有数据可用,
read()
将返回-1
,并设置errno
为EAGAIN
。
- 非阻塞写操作:如果你尝试将数据写入一个非阻塞的套接字,但该套接字的发送缓冲区已满,那么
write()
会返回-1
,并设置errno
为EAGAIN
。
当你收到 EAGAIN
错误时,通常的做法是稍后重试I/O操作,或使用像select()
、poll()
或epoll()
这样的机制来等待文件描述符变得可用
(3)实现
在目录下创建1.txt与2.txt,其内容分别如下:

relay.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h>
#define TTY1 "./1.txt" #define TTY2 "./2.txt" #define BUFSIZE 1024
enum { STATE_R = 1, STATE_W, STATE_Ex, STATE_T };
struct fsm_st { int state; int sfd; int dfd; char buf[BUFSIZE]; int len; int pos; char *errstr; };
static void fsm_driver(struct fsm_st *fsm) { int ret; switch(fsm->state) { case STATE_R: fsm->len = read(fsm->sfd, fsm->buf, BUFSIZE); if(fsm->len == 0) fsm->state = STATE_T; else if(fsm->len < 0) { if(errno == EAGAIN) fsm->state = STATE_R; else fsm->errstr = "read()"; fsm->state = STATE_Ex; } else { fsm->pos = 0; fsm->state = STATE_W; } break; case STATE_W: ret = write(fsm->dfd, fsm->buf + fsm->pos, fsm->len); if(ret < 0) { if(errno == EAGAIN) fsm->state = STATE_W; else fsm->errstr = "write()"; fsm->state = STATE_Ex; } else { fsm->pos += ret; fsm->len -= ret; if(fsm->len == 0) { fsm->state = STATE_R; } else fsm->state = STATE_W; } break; case STATE_Ex: perror(fsm->errstr); break; case STATE_T: break; default: abort(); break; } }
static void relay(int fd1,int fd2) { int fd1_save,fd2_save; struct fsm_st fsm12,fsm21; fd1_save = fcntl(fd1,F_GETFL); fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK); fd2_save = fcntl(fd2,F_GETFL); fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK); fsm12.state = STATE_R; fsm12.sfd = fd1; fsm12.dfd = fd2; fsm21.state = STATE_R; fsm21.sfd = fd2; fsm21.dfd = fd1; while(fsm12.state != STATE_T || fsm21.state != STATE_T) { fsm_driver(&fsm12); fsm_driver(&fsm21); } fcntl(fd1,F_SETFL,fd1_save); fcntl(fd2,F_SETFL,fd2_save); }
int main() { int fd1,fd2; fd1 = open(TTY1,O_RDWR); if(fd1 < 0) { perror("open()"); exit(1); } fd2 = open(TTY2,O_RDWR|O_NONBLOCK); if(fd2 < 0) { perror("open()"); exit(1); } relay(fd1, fd2); close(fd1); close(fd2); exit(0); }
|
编译运行之后,查看两个文件

二、中继引擎实例
视频教程
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include<errno.h> #include<string.h> #include"relayer.h"
#define TTY1 "../12.txt" #define TTY2 "../21.txt"
#define TTY3 "../34.txt" #define TTY4 "../43.txt"
int main() { int fd1,fd2,fd3,fd4; int job1,job2;
fd1 = open(TTY1,O_RDWR); if(fd1 < 0) { perror("open()"); exit(1); } write(fd1, "TTY1\n", 5);
fd2 = open(TTY2,O_RDWR|O_NONBLOCK); if(fd2 < 0) { perror("open()"); exit(1); } write(fd2, "TTY2\n", 5);
job1 = rel_addjob(fd1,fd2); if(job1 < 0) { fprintf(stderr,"rel_addjob():%s\n",strerror(-job1)); exit(1); }
fd3 = open(TTY3,O_RDWR); if(fd3 < 0) { perror("open()"); exit(1); } write(fd3, "TTY3\n", 5);
fd4 = open(TTY4,O_RDWR|O_NONBLOCK); if(fd4 < 0) { perror("open()"); exit(1); } write(fd4, "TTY4\n", 5);
job2 = rel_addjob(fd3,fd4); if(job2 < 0) { fprintf(stderr,"rel_addjob():%s\n",strerror(-job2)); exit(1); } while(1) pause();
close(fd1); close(fd2); close(fd3); close(fd4);
exit(0); }
|
relayer.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
| #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<pthread.h> #include <unistd.h> #include<string.h> #include<fcntl.h> #include"relayer.h"
#define BUFSIZE 1024
static struct rel_job_st *rel_job[REL_JOBMAX];
static pthread_mutex_t mut_rel_job = PTHREAD_MUTEX_INITIALIZER; static pthread_once_t init_once = PTHREAD_ONCE_INIT;
enum { STATE_R = 1, STATE_W, STATE_Ex, STATE_T };
enum{ STATE_RUNNING = 1, STATE_CANCELED, STATE_OVER };
struct rel_fsm_st{ int state; int sfd; int dfd; char buf[BUFSIZE]; int len; int pos; char *err; int64_t count; };
struct rel_job_st{ int fd1; int fd2; int job_state; struct rel_fsm_st fsm12,fsm21; int fd1_save,fd2_save; };
static void fsm_driver(struct rel_fsm_st *fsm) { int ret; switch(fsm->state) { case STATE_R: fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE); if(fsm->len == 0) { fsm->state = STATE_T; } else if(fsm->len < 0) { if(errno == EAGAIN) fsm->state = STATE_R; else fsm->err = "read()"; fsm->state = STATE_Ex; } else { fsm->pos = 0; fsm->state = STATE_W; }
break;
case STATE_W: ret = write(fsm->dfd,fsm->buf +fsm->pos,fsm->len); if(ret < 0) { if(errno == EAGAIN) fsm->state = STATE_W; else fsm->err = "wirte()"; fsm->state = STATE_Ex; } else { fsm->pos += ret; fsm->len -= ret; if(fsm->len == 0) fsm->state = STATE_R; else fsm->state = STATE_W; }
break;
case STATE_Ex: perror(fsm->err); fsm->state = STATE_T; case STATE_T: break;
default: abort(); break; } }
static void *thr_relayer(void *p) { int i; while(1) { pthread_mutex_lock(&mut_rel_job); for(i = 0;i<REL_JOBMAX;i++) { if(rel_job[i] != NULL) { if (rel_job[i]->job_state == STATE_RUNNING) { fsm_driver(&rel_job[i]->fsm12); fsm_driver(&rel_job[i]->fsm21); if(rel_job[i]->fsm12.state == STATE_T && rel_job[i]->fsm21.state == STATE_T) rel_job[i]->job_state = STATE_OVER; } } } pthread_mutex_unlock(&mut_rel_job); } }
static void module_load(void) { int err; pthread_t tid_relayer;
err = pthread_create(&tid_relayer,NULL,thr_relayer,NULL); if(err) { fprintf(stderr,"pthread_create():%s\n",strerror(err)); exit(1); } }
static int get_free_pos_unlocked() { int i; for(i = 0;i<REL_JOBMAX;i++) { if(rel_job[i] == NULL) return i; } return -1; }
int rel_addjob(int fd1,int fd2) { struct rel_job_st *me; int pos;
pthread_once(&init_once,module_load); me = malloc(sizeof(*me)); if (me == NULL) { return -ENOMEM; } me->fd1 = fd1; me->fd2 = fd2; me->job_state = STATE_RUNNING;
me->fd1_save = fcntl(me->fd1, F_GETFL); fcntl(me->fd1, F_SETFL, me->fd1_save | O_NONBLOCK);
me->fd2_save = fcntl(me->fd2, F_GETFL); fcntl(me->fd2, F_SETFL, me->fd2_save | O_NONBLOCK);
me->fsm12.sfd = me->fd1; me->fsm12.dfd = me->fd2; me->fsm12.state = STATE_R;
me->fsm21.sfd = me->fd2; me->fsm21.dfd = me->fd1; me->fsm21.state = STATE_R; pthread_mutex_lock(&mut_rel_job); pos = get_free_pos_unlocked(); if (pos < 0) { pthread_mutex_unlock(&mut_rel_job); fcntl(me->fd1, F_SETFL, me->fd1_save); fcntl(me->fd2, F_SETFL, me->fd2_save); free(me); return -ENOSPC; } rel_job[pos] = me; pthread_mutex_unlock(&mut_rel_job); return pos;
}
int rel_canceljob(int id) {
}
int rel_waitjob(int id, struct rel_stat_st *rel) {}
int rel_statjob(int id, struct rel_stat_st *rel) {}
|
relayer.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #ifndef RELAYER_H_ #define RELAYER_H_
#include <stdio.h>
#define REL_JOBMAX 1000
struct rel_stat_st{ int state; int fd1; int fd2; int64_t count12,count21; };
int rel_addjob(int fd1,int fd2);
int rel_canceljob(int id);
int rel_waitjob(int id,struct rel_stat_st *);
int rel_statjob(int id,struct rel_stat_st *);
#endif
|
1 2 3 4 5 6 7 8
| cmake_minimum_required(VERSION 3.16) project(CALC) # 工程名字可以与可执行程序名不一样 add_executable(main main.c relayer.c relayer.h)
# 查找pthread库 find_package(Threads REQUIRED) # 链接pthread库到你的目标 target_link_libraries(main Threads::Threads)
|
三、高级IO
1.IO多路转接(多路复用)
监视多个文件描述符
IO多路转接允许单个进程监视多个文件描述符以检查它们是否已准备好进行读取或写入操作。这是非阻塞IO操作的一种形式,常用于提高对多个连接或数据流的同时处理能力
IO多路转接的主要应用是在网络编程中,尤其是在服务器中,它可以处理大量的并发连接。例如,一个Web服务器可能需要同时处理数千个连接,每个连接都可能在不同的时间请求数据。通过使用IO多路转接,服务器可以有效地管理这些连接,而无需为每个连接启动一个单独的线程或进程
让我们考虑一个简单的场景:假设你是一个服务器,你有多个客户端连接到你。每个连接都是一个文件描述符。客户端可能会在任何时间发送数据,所以你需要知道哪个客户端已经发送了数据,以便你可以读取它。使用阻塞IO,你只能一次处理一个连接,这是低效的。但使用IO多路转接,你可以监视所有连接,并当数据可用时立即处理它。
(1)select()
是最古老的IO多路转接解决方案,它允许应用程序监视多个文件描述符的状态
该函数若不设置超时时间,这个函数就会一直死等(阻塞)
1 2 3
| #include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
|
1 2 3 4
| struct timeval{ long tv_sec; long tv_usec; };
|
参数:
nfds
: 最大的文件描述符编号加1。比如,如果你要监视的文件描述符是3、4和5,那么nfds
应该是6
readfds
: 一个文件描述符集合,用于检查哪些描述符准备好进行读取
writefds
: 一个文件描述符集合,用于检查哪些描述符准备好进行写入
exceptfds
: 一个文件描述符集合,用于检查哪些描述符有异常条件发生
timeout
: 指定select()
等待的最大时间。如果为NULL,那么select()
会无限期等待(不进行超时设置,函数会进行阻塞)
fd_set
是一个特殊的数据类型,用于表示文件描述符集合。你可以使用以下宏来操作它:
FD_ZERO(fd_set *set)
: 清除一个文件描述符集合。
FD_SET(int fd, fd_set *set)
: 将一个文件描述符添加到集合中。
FD_CLR(int fd, fd_set *set)
: 从集合中删除一个文件描述符。
FD_ISSET(int fd, fd_set *set)
: 检查一个文件描述符是否在集合中。
返回值:
−1
: 出错。
0
: 超时,指定的时间内没有任何文件描述符准备好。
>0
: 准备好的文件描述符的数量。
(2)poll()
与 select 类似,但提供了一个更灵活的接口
poll
函数是一个用于监控一组文件描述符的状态变化的系统调用。它是网络编程和系统编程中常用的一个功能。这个函数允许程序监控多个文件描述符,以查看是否有一个或者多个文件描述符准备好进行输入输出操作
在使用 poll
函数时,检查其返回值是非常重要的,因为它告诉你函数调用的结果以及是否有文件描述符准备好进行IO操作。如果 poll
返回一个正数,那么你需要遍历文件描述符集合,检查哪些描述符的 revents
字段指示了实际发生的事件,然后据此执行相应的IO操作。
函数原型
1 2 3
| #include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
|
fds
: 指向一个 pollfd
结构体数组的指针,每个结构体用于指定一组文件描述符和需要监控的事件。
nfds
: 指定数组 fds
中结构体的数量。
timeout
: 等待的毫秒数,如果设置为负数,则表示无限等待,直到某个文件描述符准备好。timeout
写-1
表示阻塞死等,写0
表示阻塞
返回值
>0
:表示成功,这个正数描述的是有多少文件描述符实际上满足了调用中请求的条件。换句话说,它告诉你有多少个文件描述符准备好了进行IO
操作
0
:如果 poll
函数返回0
,这意味着没有文件描述符在指定的超时时间内准备好进行IO
操作。这通常表示操作超时,需要根据应用程序的逻辑来处理这种情况
-1
:当 poll
函数返回-1
时,这通常表示发生了错误。在这种情况下,可以检查 errno
来获取错误的具体原因
1 2 3 4 5
| struct pollfd { int fd; short events; short revents; };
|
其中,events
字段用于指定需要监控的事件类型,比如:
POLLIN
: 表示数据可读。
POLLOUT
: 表示数据可写。
POLLERR
: 表示错误。
如何使用:
创建一个struct pollfd
形式的数组,大小为n
(表示监视了n
个文件描述符),并且用struct pollfd *fds
类型的指针指向。数组中每一个成员均是一个struct pollfd
类型的结构体,里面包含文件描述符,与用户关系关心的事件(需要监控的事件),以及已发生的事件。
(3)epoll()
是 Linux 特定的(方言),提供了一个更高效的方式来处理大量的文件描述符
若编写的程序考虑移植性时,则不能使用epoll
。应该使用poll
或者select
在C语言中,epoll
是Linux系统提供的一种高效的多路复用IO接口,它是专门设计用来处理大量并发连接的。epoll
比传统的 select
和 poll
接口更为高效,主要是因为它在处理大量文件描述符时不会有显著的性能下降。epoll
在网络编程,尤其是在高性能服务器的设计中非常重要
epoll
的基本使用包括以下几个步骤:
创建 epoll
实例:
通过调用 epoll_create
函数来创建一个 epoll
的实例,打开一个epoll
文件描述符
size
参数在较新的Linux内核中已经不再使用,但必须提供一个大于0
的数值,(size
并不表示,监视文件描述符的个数,只需要对函数传入一个正数即可)
1 2 3
| #include <sys/epoll.h>
int epoll_create(int size);
|
返回值
>0
:如果 epoll_create
函数返回一个正数,这个数值是新创建的 epoll
实例的文件描述符。这个描述符后续用于所有针对该 epoll
实例的操作,比如在调用 epoll_ctl
和 epoll_wait
时
<0
:当 epoll_create
返回-1时,表示创建 epoll
实例失败。这种情况通常是由于某些错误,比如资源限制、无效的参数或内部错误。在这种情况下,可以检查 errno
来获取错误的具体原因
控制 epoll
监听的文件描述符:
通过调用 epoll_ctl
函数来添加、修改或删除要监控的文件描述符
1
| int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
|
相当于对poll
函数封装了一层,不让用户直接操作poll
函数中的struct pollfd
形式的数组,而是给用户提供方法告诉系统,用户的需求,系统帮用户进行管理
epfd
:由 epoll_create
返回的 epoll
实例的文件描述符。
op
:要进行的操作,比如 EPOLL_CTL_ADD
(添加描述符)、EPOLL_CTL_MOD
(修改描述符)、EPOLL_CTL_DEL
(删除描述符)。
fd
:要操作的文件描述符。
event
:指向 epoll_event
结构的指针,该结构指定了要监控的事件和关联的数据
等待事件的发生:
通过调用 epoll_wait
函数来等待一个或多个文件描述符上的事件
1
| int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
|
epfd
:epoll
实例的文件描述符。
events
:用于从内核获取事件的 epoll_event
结构数组。
maxevents
:告诉内核这个 events
数组可以接受多少个 epoll_event
。
timeout
:等待事件的最大毫秒数,如果设置为-1,则表示无限等待。
epoll_event
结构通常定义如下:
1 2 3 4
| struct epoll_event { uint32_t events; epoll_data_t data; };
|
events
字段用于指定感兴趣的事件和发生的事件。
EPOLLIN
: 表示对应的文件描述符可读(包括普通文件描述符、管道和套接字)。如果有新的连接请求,也会触发这个事件
EPOLLOUT
: 表示对应的文件描述符可写
EPOLLPRI
: 表示对应的文件描述符有紧急的数据可读(这通常用于TCP套接字上的带外数据)
EPOLLERR
: 表示对应的文件描述符发生错误。epoll_wait
会始终等待此事件,即使没有明确指定
EPOLLHUP
: 表示对应的文件描述符被挂断。同样,epoll_wait
也总是等待此事件
EPOLLET
: 设置边缘触发行为,这意味着事件只在状态发生变化时通知一次
data
字段通常用于存储用户定义的数据,例如文件描述符、指针等。
epoll
相比 select
和 poll
的优势在于它不需要每次调用时都重新传入整个监听列表,而且在活动连接数很多的情况下,epoll
的效率要高得多。这使得 epoll
成为处理大规模并发连接的首选方法,特别是在构建高性能网络服务器时。
四、内存映射
1.readv与writev
在C语言中,readv
和 writev
函数属于散布/聚集I/O操作,用于在单个系统调用中从文件描述符读取多个非连续缓冲区(readv
)或将多个非连续缓冲区写入文件描述符(writev
)。这些函数定义在 POSIX
标准中,常用于网络编程和文件操作,提高了处理多个缓冲区的效率
多个碎片地址,并非连续地址空间,可以使用readv
一次将文件描述符中的内容读取至碎片地址中;或者使用writev
将碎片地址的内容写入文件描述符中
(1)readv函数
readv
函数从文件描述符读取数据到多个缓冲区。其函数原型如下:
1 2 3
| #include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
|
fd
: 文件描述符,指向要读取的文件或套接字。
iov
: 指向一个或多个 iovec
结构的指针,这些结构定义了缓冲区的位置和大小。
iovcnt
: iov
数组中的元素数量。
iovec
结构体定义如下:
1 2 3 4
| struct iovec { void *iov_base; size_t iov_len; };
|
(2)writev函数
writev
函数将多个缓冲区的数据写入文件描述符。其函数原型如下:
1 2 3
| #include <sys/uio.h>
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
|
fd
: 文件描述符,指向要写入的文件或套接字。
iov
: 指向一个或多个 iovec
结构的指针,这些结构定义了要写入的数据的位置和大小。
iovcnt
: iov
数组中的元素数量
返回值
对于 readv
和 writev
函数,返回值是读取或写入的字节数。如果出现错误,函数返回-1,并设置 errno
以指示错误类型。如果在读取过程中到达文件末尾,readv
可能返回小于请求的字节数。
使用场景
这些函数的优势在于能够在单个系统调用中处理多个缓冲区,这减少了系统调用的开销并提高了效率。在网络编程中,尤其是在需要读取或发送格式化的数据(如多个字段的协议消息)时,readv
和 writev
非常有用。
2.内存映射IO
内存映射I/O(Memory-mapped I/O)
是一种在程序中处理文件的高效方式,它将文件的内容直接映射到进程的地址空间。这种方法通过内存操作代替传统的文件I/O
调用(如read
和write
),提高了文件操作的效率,并简化了文件内容的处理。
工作原理
内存映射I/O通过将一个文件或者其他对象(比如设备)映射到进程的虚拟地址空间来工作。这意味着文件的一部分或全部直接映射到内存地址,应用程序可以像访问普通内存那样访问这些文件数据。
关键函数
(1)mmap函数
在Unix和类Unix系统(如Linux)中,内存映射I/O主要通过mmap
函数实现:
1 2 3
| #include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
addr
:建议的映射起始地址(通常设为NULL,由系统选择地址)。
length
:映射区域的长度。
prot
:映射区域的保护方式,比如PROT_READ
(可读)、PROT_WRITE
(可写)。
flags
:指示映射类型的标志,比如MAP_SHARED
(对文件的修改影响到原文件)或MAP_PRIVATE
(对文件的修改不影响原文件)。
fd
:文件描述符,指向要映射的文件。
offset
:文件中的偏移量,映射从此处开始。
返回值
- 成功时:当
mmap
成功时,它返回映射区域的起始地址。这个地址是一个指向 void
的指针,可以被转换为任何类型的指针以便于数据访问。
- 失败时:如果映射失败,
mmap
返回 MAP_FAILED
,这在大多数系统中定义为 (void *)-1
。当映射失败时,通常需要检查 errno
以确定失败的原因,可能的错误原因包括无效的文件描述符、不允许的内存保护等级、映射区域过大等。
(2)mnumap函数
使用munmap
函数来撤销映射:
1
| int munmap(void *addr, size_t length);
|
addr
: 函数mmap
成功返回映射区域的起始地址
length
: 映射区域的长度
使用mmap
与mnumap
函数优势:
- 性能提升:减少了系统调用和内核空间到用户空间的数据拷贝,提高了数据访问的效率。
- 简化操作:可以直接使用指针操作文件数据,无需繁琐的读写函数调用。
- 动态加载:只有实际访问的内存页才会被加载,对于大文件处理尤为高效。
- 共享内存:通过
mmap
可以实现不同进程之间的内存共享。
(3)实例程序
程序目标:指定任意的文件,对文件内容中的字符a
进行计数
目标文件为1.txt
,其内容如下:

main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
#include<stdlib.h> #include <stdio.h> #include <sys/mman.h> #include <fcntl.h>
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h>
int main(int argc,char* argv[]) { int fd,count = 0; struct stat statres; char *str; if(argc < 2) { fprintf(stderr,"USage...\n"); exit(1); }
fd = open(argv[1],O_RDONLY); if(fd < 0) { perror("open()"); exit(1); }
if(fstat(fd,&statres) < 0) { perror("fstat()"); exit(1); }
str = mmap(NULL,statres.st_size,PROT_READ,MAP_SHARED,fd,0); if(str == MAP_FAILED) { perror("mmap()"); exit(1); }
close(fd);
for(int i = 0;i<statres.st_size;i++) { if(str[i]=='a') { count++; } } printf("文件:%s,字符a个数为:%d\n",argv[1],count);
munmap(str,statres.st_size);
exit(0); }
|
CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 11
| cmake_minimum_required(VERSION 3.20) project(Adv_IO C)
set(CMAKE_C_STANDARD 99)
# 查找pthread库 # find_package(Threads REQUIRED)
add_executable(adv_io main.c)
# target_link_libraries(adv_io Threads::Threads)
|
运行结果

(4)创建匿名映射
创建匿名映射
要创建一个匿名映射,通常需要在 mmap
调用中设置 MAP_ANONYMOUS
(或在某些系统中是 MAP_ANON
)标志,并将文件描述符设置为 -1
。同时,通常与 MAP_SHARED
或 MAP_PRIVATE
标志结合使用,分别用于创建可共享或仅当前进程可用的内存区域
1 2 3
| #include <sys/mman.h>
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
|
NULL
表示让系统决定映射区域的起始地址。
size
是要映射的内存大小。
PROT_READ | PROT_WRITE
设置内存区域为可读写。
MAP_ANONYMOUS | MAP_SHARED
表示这是一个匿名且仅此进程可用的映射。
- 文件描述符
-1
和偏移量 0
用于匿名映射。
用途
匿名映射的一些常见用途包括:
- 替代
malloc
或其他内存分配函数:用于动态分配一大块内存,尤其是当需要的内存大小超过了 malloc
等标准函数的限制时。
- 进程间通信:通过创建共享的匿名映射,不同的进程可以访问和修改同一块内存区域,这对于共享数据或实现进程间通信非常有用。
- 缓冲区或临时存储:用于创建一个缓冲区,特别是当涉及到大量数据且需要避免频繁的堆分配时。
注意事项
- 使用匿名映射时,映射的内存区域通常初始化为零。
- 与任何通过
mmap
创建的映射一样,使用完毕后应该通过 munmap
来释放映射。
- 匿名映射是操作系统特定的功能,主要在类Unix系统(如Linux)中可用。
(5)mmap实现父子进程的内存共享
使用mmap
实现父子进程的进程通信
思路:使用mmap中的匿名映射,使用 mmap
函数进行匿名映射(Anonymous Mapping)时,映射的内存区域不与任何文件关联。这种映射通常用于分配一块可以被多个进程共享的内存,或者仅在当前进程中作为一种动态分配的、可调整大小的内存区域。在父进程中使用mmap函数在父进程中进行匿名映射,在使用fork函数创建出子进程,子进程会将父进程的资源进行复制,因此从文件中映射的内容也会进行复制,这个时候就可以通过mmap函数进行进程间的通信。
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
#include <stdio.h> #include <stdlib.h> #include <sys/mman.h>
#include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h>
#define MEMSIZE 1024
int main() { char *ptr; pid_t pid; ptr = mmap(NULL, MEMSIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); if(ptr == MAP_FAILED) { perror("mmap()"); exit(1); }
pid = fork(); if(pid < 0) { perror("fork()"); munmap(ptr,MEMSIZE); exit(1); }
if(pid == 0) { strcpy(ptr,"HELLO!"); munmap(ptr,MEMSIZE); exit(0); }else { wait(NULL); puts(ptr); munmap(ptr,MEMSIZE); exit(0); }
exit(0); }
|
运行结果:

3.文件锁
文件锁是一种用于控制对文件或文件一部分的访问的机制,主要用于同步多个进程或线程对同一文件的访问。在多用户或多任务的操作系统中,文件锁非常重要,因为它们可以防止同时运行的进程互相干扰,从而确保数据的一致性和完整性
主要类型
实现方法
(1)lockf函数
lockf
函数
1 2 3
| #include <unistd.h>
int lockf(int fd, int cmd, off_t len);
|
fd
:文件描述符,指向要锁定或解锁的文件。
cmd
:操作命令,指定要执行的锁定操作。
len
:锁定或解锁的字节数。
操作命令
cmd
参数支持以下几种操作:
F_LOCK
:对指定的文件区域加上排他锁。如果锁已被其他进程持有,lockf
会阻塞,直到能够获取锁。
F_TLOCK
:尝试对指定文件区域加上排他锁。如果锁已被其他进程持有,函数会立即返回,不会阻塞。
F_ULOCK
:解锁指定的文件区域。
F_TEST
:测试指定的文件区域是否被其他进程锁定。
返回值
- 成功时:返回0。
- 失败时:返回-1,并设置
errno
来指示错误原因。例如,如果文件描述符无效,errno
可能被设置为 EBADF
。
(2)实例程序
多进程并发对文件../out
,中的数字进行逐次+1
,若最开始文件中数字为1
,程序运行结束之后数字应为20
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
|
#include <stdio.h> #include <stdlib.h>
#include <string.h> #include <unistd.h>
#include <sys/types.h> #include <sys/wait.h>
#define PROCNUM 20 #define FNAME "../out" #define LINESIZE 1024
static void func_add(void) { int fd; FILE *fp; char linebuf[LINESIZE];
fp = fopen(FNAME,"r+"); if(fp ==NULL) { perror("fopen()"); exit(1); }
fd = fileno(fp); if(fd < 0) { perror("fileno()"); exit(1); }
lockf(fd,F_LOCK,0);
fgets(linebuf,LINESIZE,fp); fseek(fp,0,SEEK_SET); sleep(1); fprintf(fp,"%d\n",atoi(linebuf)+1); fflush(fp); lockf(fd,F_ULOCK,0);
fclose(fp);
return; }
int main() { int i; pid_t pid;
for(i = 0;i<PROCNUM;i++) { pid = fork(); if(pid < 0) { perror("fork()"); exit(1); } if(pid == 0) { func_add(); exit(0); } }
for(i = 0;i<PROCNUM;i++) { wait(NULL); }
exit(0); }
|
fileno
函数
fileno
函数是一个标准库函数,用于将文件流(例如,由 fopen
创建的 FILE*
类型的对象)转换成对应的文件描述符。这个函数主要在需要将高级I/O函数(如 fprintf
, fscanf
)与低级I/O函数(如 read
, write
)混合使用的场景中非常有用
fileno
函数的原型定义在 <stdio.h>
头文件中,如下所示:
1 2 3
| #include <stdio.h>
int fileno(FILE *stream);
|
五、管道
管道可以看作队列
管道(Pipe)是一种用于在进程之间传输数据的机制。管道可以被视为一个特殊的文件,其中一个进程可以写入数据,而另一个进程则从中读取数据。管道主要用于实现进程间通信(IPC,Inter-Process Communication),尤其是在类Unix操作系统(如Linux)中。
管道分为两种主要类型:匿名管道(Anonymous Pipes)和命名管道(Named Pipes 或 FIFOs)。
匿名管道(Anonymous Pipes)
- 用途:仅用于有父子关系的进程间通信。
- 特点:它们是临时的,只存在于进程间通信的持续期间。
- 实现:在Unix/Linux系统中,通常通过
pipe()
系统调用创建。一个进程创建管道后,它会产生两个文件描述符:一个用于读取,另一个用于写入。
命名管道(Named Pipes 或 FIFOs)
- 用途:可用于任何两个希望通信的进程之间,无需具有父子关系。
- 特点:以文件形式存在于文件系统中,因此它们在创建后可以被任何进程访问。
- 实现:在Unix/Linux系统中,通常通过
mkfifo
命令或mkfifo()
系统调用创建。