第五章 文件与I/O(2)
文件与I/O
read、write
ㅤ ㅤ一旦有了与一个打开文件描述相连的文件描述符,只要该文件是用O_RDONLY或O_RDWR标志打开的,就可以用read()系统调用从该文件中读取字节
read
函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数
fd :想要读的文件的文件描述符
buf : 指向内存块的指针,从文件中读取来的字节放到这个内存块中
count : 从该文件复制到buf中的字节个数
返回值
如果出现错误,返回-1
读文件结束,返回0
否则返回从该文件复制到规定的缓冲区中的字节数
注意:
每次调用read()函数,会从fd指向的文件的当期偏移开始读取len字节到buf所指向的内存中,fd的文件位置指针会向前移动,移动的长度由读取到的字节数决定;如果fd多指向的对象不支持seek操作,则读操作总是从“当前”位置开始
write
ㅤ ㅤ用 write() 系统调用将数据写到一个文件中
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
函数参数:
fd:要写入的文件的文件描述符
buf: 指向内存块的指针,从这个内存块中读取数据写入到文件中
count: 要写入文件的字节个数
返回值
如果出现错误,返回-1
如果写入成功,则返回写入到文件中的字节个数,并更新文件位置
返回0,没有任何特殊意义,只是表示写入了零个字节
注意:
不支持 seek 的文件总是从起始位置开始写
CP
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
int main(int argc, char* argv[])
{
int infd,outfd;
if(argc != 3)
{
fprintf(stderr,"Usage %s src dest\n",argv[0]);
exit(EXIT_FAILURE);
}
if((infd = open(argv[1],O_RDONLY)) == -1)
ERR_EXIT("open src error");
if((outfd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0644)) == -1)
ERR_EXIT("open dest error");
char buf[1024];
int nread;
while ((nread = read(infd, buf, 1024)) > 0 )
{
write(outfd, buf, nread);
}
close(infd);
close(outfd);
return 0;
}
注意*
对于read的返回值有很多可能性
- block_read
ssize_t ret;
if(len > SSIZE_MAX)
len = SSIZE_MAX;
while(len!=0 && (ret=read(fd,buf,len)) !=0 ){
if(ret == -1){
if(errno == EINTR)
continue;
perror("read");
break;
}
len -= ret;
buf += ret;
}
- unblock_read
char buf[BUFSIZ];
ssize_t nr;
if(len > SSIZE_MAX)
len = SSIZE_MAX;
start:
nr = read(fd,buf,BUFSIZ);
if(nr == -1){
if(errno == EINTR)
goto start;
if(errno == EAGAIN)
/* resubmit later */
else
/* error */
}
- write
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;
}
lseek
文件随机读写
ㅤ 到目前为止的所有文件访问都是顺序访问。这是因为所有的读和写都从当前文件的偏移位置开始,然后文件偏移值自动地增加到刚好超出读或写结束时的位置,使它为下一次访问作好准备。
ㅤ 有个文件偏移这样的机制,在 Linux 系统中,随机访问就变得很简单,你所需做的只是将当前文件移值改变到有关的位置,它将迫使一次 read() 或 write() 发生在这一位置。(除非文件被 O_APPEND 打开,在这种情况下,任何 write 调用仍将发生在文件结束处)
- lseek
ㅤ 功能说明:通过指定相对于开始位置、当前位置或末尾位置的字节数来重定位 curp,这取决于 lseek() 函数中指定的位置
原型:
off_t lseek(int fd, off_t offset, int whence);
参数:
fd : 需设置的文件标识符
offset : 偏移量
whence : 搜索的起始位置
SEEK_SET : 从文件开始处计算偏移
SEEK_CUR : 从当前文件的偏移值计算偏移
SEEK_END : 从文件的结束处计算偏移
返回值:
返回新的文件偏移值
定位读写
pread
功能:
定位读
原型:
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
参数:
fd : 文件描述符
buf : 指向内存块的指针,从文件中读取来的字节放到这个内存块中
count : 要读取的字节数
pos : 从文件描述符的pos位置开始读
返回值:
成功:读到的字节数
0 : EOF
出错 : -1 ,errno被设置
pwrite
功能:
定位写
原型:
ssize_t pwrite(int fd, void *buf, size_t count, off_t offset);
参数:
fd : 文件描述符
buf : 指向内存块的指针,从这个内存块中读取数据写入到文件中
count : 要写入的字节数
pos : 从文件描述符的pos位置开始写
返回值:
成功:写入的字节数
0 : 什么都没写
出错 : -1 ,errno被设置
pread/pwrite 和 lseek 区别
- pread() 和 pwrite() 调用更易于使用,尤其是对于一些复杂的操作,比如在文件中反向或随机查找定位
- pread() 和 pwrite() 调用在结束时不会修改文件的位置指针
- pread() 和 pwrite() 调用避免了在使用 lseek() 时会出现的竞争
I/O多路复用
ㅤ 应用通常需要在多个文件描述符上阻塞,如果不使用线程,而是独立处理每个文件描述符,单个进程无法同时在多个文件描述符上阻塞;如果一个文件描述符没有数据,进程就会一直阻塞,后面的程序进没法执行;非阻塞 I/O 是一种处理方式,应用发送 I/O 请求,该请求返回特定错误,而不是阻塞;这种方式效率不高,主要有两点原因:
- 进程需要连续随机发送 I/O 操作,等待某个打开的文件描述符可以执行 I/O 操作
- 如果进程睡眠则会更高效,睡眠可以释放 CPU 资源,使得 CPU 可以处理其他任务,直到一个或多个文件描述符可以执行 I/O 时再唤醒进程
多路I/O设计的原则
- I/O 多路服用:当任何一个文件描述符 I/O 就绪时进行通知
- 都不可用?在有可用的文件描述符之前一直处于睡眠状态
- 唤醒:哪个文件描述符可用了
- 处理所有 I/O 就绪的文件描述符,没有阻塞
- 返回第1步,重新开始
select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_ZERO
功能:
从指定的集中删除所有的文件描述符
原形:
void FD_ZERO(fd_set *set);
参数:
set : 要删除的文件描述符的集合
注意:
每次调用select之前都应该调用该宏
FD_SET
功能:
向指定集中添加一个文件描述符
原形:
void FD_SET(int fd, fd_set *set);
参数:
fd : 要添加的文件描述符
set: 文件描述符集
FD_CLR
功能:
从指定集中删除一个文件描述符
原形:
void FD_CLR(int fd, fd_set *set);
参数:
fd : 要删除的文件描述符
set: 文件描述符集
注意:
设计良好的代码应该都不需要使用FD_CLR,极少使用该宏
FD_ISSET
功能:
检查一个文件描述符是否在给定的集中
原形:
int FD_ISSET(int fd, fd_set *set);
参数:
fd : 文件描述符
set: 文件描述符集
返回值:
成功 : 非0
失败 : 0
示例
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#define TIMEOUT 5
#define BUF_LEN 1024
int main(int argc, char const *argv[])
{
int ret;
fd_set readfds;
struct timeval timeout;
timeout.tv_sec = TIMEOUT;
timeout.tv_usec = 0;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
ret = select(STDIN_FILENO+1,&readfds, NULL, NULL, &timeout);
if(ret == -1)
{
perror("select");
exit(EXIT_FAILURE);
}
else if(ret == 0)
{
printf("timeout...\n");
}
else if(ret)
{
if(FD_ISSET(STDIN_FILENO, &readfds))
{
char buf[BUF_LEN]={0};
ret = read(STDIN_FILENO, buf, BUF_LEN);
if(ret == -1)
{
perror("read");
exit(EXIT_FAILURE);
}
if(ret)
{
buf[ret] = '\0';
printf("buf : %s\n", buf);
}
}
}
return 0;
}
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define TIMEOUT 5
#define BUF_LEN 1024
int main(int argc, char const *argv[])
{
int ret;
fd_set readfds;
struct timeval timeout;
int keyBoard_fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if (keyBoard_fd < 0)
{
perror("open");
exit(EXIT_FAILURE);
}
while (1)
{
timeout.tv_sec = TIMEOUT;
timeout.tv_usec = 0;
FD_ZERO(&readfds);
FD_SET(keyBoard_fd, &readfds);
ret = select(keyBoard_fd + 1, &readfds, NULL, NULL, &timeout);
if(ret == -1)
{
perror("select");
exit(EXIT_FAILURE);
}
else if(ret == 0)
{
printf("timeout...\n");
}
else if(ret)
{
if (FD_ISSET(keyBoard_fd, &readfds))
{
int ch;
ret = read(keyBoard_fd, &ch, 1);
if(ret == -1)
{
perror("read");
fflush(stdin);
continue;
}
if(ch == '\n')
continue;
printf("ch : %c\n",ch);
if(ch == 'q')
break;
}
}
}
return 0;
}
pselect
pselect 和 select 存在的三点区别:
- pselect 的 timeout 参数使用了 timespec 结构体,而不是 timeval 结构体。timespec 结构体使用秒和纳秒,而不是秒和毫秒,从理论上讲更加精确些。但实际上,这两个结构体在毫秒精度上已经不可靠了
- pselect 调用不会修改 timeout 参数,因此,在后续调用中,不需要重新初始化该参数
- select 系统调用没有 sigmask 参数,当 pselect 这个参数设置为 null 时,peselect 的行为和 select 相同
poll
功能:
wait for some event on a file descriptor
原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds : pollfd结构体数组
nfds : 要监控的结构体数组的个数
timeout : 指定等待的时间长度,单位是毫秒,不论是否有I/O就绪,poll调用都会返回,
timeout值为负数,表示永远等待
timeout为0表示poll调用立即返回,并给出所有I/O未就绪的文件描述符列表,不会等待更多事件,这种情况,poll调用如同其名,轮训一次后立即返回
返回值:
成功 :poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;
失败 :poll() 返回 -1,并设置 errno 为下列值之一:
ㅤ ㅤ由于select使用了基于文件描述符的三位掩码的解决方案,其效率不高;poll使用了nfds个pollfd结构体构成的数组,fds指针指向该数组,pollfd结构体定义如下
struct pollfd{
int fd; /*file descriptor*/
short event; /*requested events to watch */
short revents; /*returned events witnessed */
};
ㅤ ㅤ每个 pollfd 结构体指定一个被监视的文件描述符。可以给 poll 传递多个 pollfd 结构体,使它能够监视多个文件描述符。每个结构体的 events 变量是要监视的文件描述符的事件的位掩码。用户可以设置该变量。revents 变量是该文件描述符的结果事件的位掩码。内核在返回时会设置revents 变量。events 变量中请求的所有事件都可能在 revents 变量中返回;
events | 事件 |
---|---|
POLLIN | 普通或优先级带数据可读 |
POLLRDNORM | 普通数据可读 |
POLLRDBAND | 优先级带数据可读 |
POLLPRI | 高优先级数据可读 |
POLLOUT | 普通数据可写 |
POLLWRNORM | 普通数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 描述字不是一个打开的文件 |
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#define TIMEOUT 5
int main(int argc, char const *argv[])
{
int ret;
struct pollfd fds[2];
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = STDOUT_FILENO;
fds[1].events = POLLOUT;
ret = poll(fds, 2, TIMEOUT*1000);
if(ret == -1)
{
perror("poll");
exit(EXIT_FAILURE);
}
if(ret == 0)
{
printf("%d second elapsed.\n", TIMEOUT);
return 0;
}
if(fds[0].revents & POLLIN)
printf("stdin is readable.\n");
if(fds[1].revents & POLLOUT)
printf("stdout is writable.\n");
return 0;
}
poll 优点
- poll里包含了要监视的 event 和发生的 event,使读写分离,每次循环不需要重新设置,接口使用方便
- poll 的文件描述符数量不受限制,一个数组可以开多大,就可以有多少个文件描述符,换句话说,只要内存足够,想开多少就开多少,但是太多了性能也会下降
poll缺点
- 当文件描述符很多的时候,依旧需要使用轮询的方式来获取文件描述符,效率低
- 每次调用 poll 也需要把大量的 pollfd 结构从用户态拷贝至内核中,文件描述符很多事,效率也会很低
- 同时连接的大量客户在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会现行下降
ppoll
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *timeout_ts, const sigset_t *sigmask);
ㅤ ㅤ类似于 pselect,timeout 参数指定的超时时间是秒和纳秒,sigmask 参数提供了一组等待处理的信号
poll 和 select 的区别
- poll 不需要用户计算最大文件描述符值加 1 作为参数传递给它
- poll 对于值很大的文件描述符,效率更高,select 监视值为 900 的文件描述符,内核需要检查每个集合中的每个位,一直检查 900 个位
- select 的文件描述符集合是静态的,需要对大小设置进行权衡;如果值很小,会限制select可监视的最大文件描述符值;如果值很大,效率会很低,对于 poll,可以准确创建大小合适的数组。如果只需要监视一项,则仅传递一个结构体
- 对于 select 调用,返回时会重新创建文件描述符集,因此每次调用都必须重新初始化。poll 系统调用会把输入(events)和输出(revents)分离开,支持无需改变数组就可以重新使用
- select 调用 timeout 参数在返回时是未定义的,代码要支持可移植,需要重新对它初始化,而对 pselect 不存在这些问题
select 优点
- select 可移植性更好,因为有些 unix 系统不支持 poll
- select 提供了更高的超时精度:select 支持微妙级,poll 支持毫秒级。ppoll 和 pselect 理论上都提供了纳秒精度,但是这两个调用的毫秒级精度都不可靠
标准I/O
打开流
fopen
功能:
打开一个流
原型:
FILE *fopen(const char *path, const char *mode);
参数:
path : 文件路径
mode : 模式
返回值:
成功 : 将返回一个指向文件对象 FILE 的指针
失败 : NULL
- 打开方式
打开方式 | 说明 |
---|---|
"r" | 以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。 |
"w" | 以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
"a" | 以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
"r+" | 以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。 |
"w+" | 以“写入/更新”方式打开文件,相当于w和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
"a+" | 以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
- 控制读写方式的字符串(可以不写)
打开方式 | 说明 |
---|---|
"t" | 文本文件。如果不写,默认为"t"。 |
"b" | 二进制文件。 |
fdopen
功能:
将一个流关联到已存在的文件描述符
原型:
FILE *fdopen(int fildes, const char *mode);
参数:
fildes : 已存在的文件描述符
mode : 取值为 ("r","r+","w","w+","a", "a+"之一) 必须与文件描述符的模相匹配
返回值:
成功 : 将返回一个指向文件对象 FILE 的指针
失败 : NULL
关闭流
fclose
功能:
关闭流
原型:
int fclose(FILE *stream);
参数:
stream : 指向文件对象 FILE 的指针
返回值:
成功 0
失败 EOF 并设置全局变量 errno
从流中读数据
每次读取一个字节
fgetc
功能:
从流中读一个字节的数据
原型:
int fgetc(FILE *stream);
参数:
stream : 指向文件对象 FILE 的指针
返回值:
成功 : 返回以无符号字符形式读取的字符,在文件结束将其转换为int。
错误 : EOF
ungetc
功能:
把一个字节的数据放入流中
原型:
int ungetc(int c, FILE *stream);
参数:
c : 要放入的字符
stream : 指向文件对象 FILE 的指针
返回值:
成功 : c
错误 : EOF
每次读取一行
fgets
功能:
从流中读一个字符串
原型:
char *fgets(char *s, int size, FILE *stream);
参数:
s : 存放读取数据的buf
size :要读取的字节数
stream : 指向文件对象 FILE 的指针
返回值:
成功 : str
错误 : NULL
说明:
从stream中读取size-1个字节的数据,并把结果保存到str中。读完最后一个字节后,缓冲区中会写入空字符(\0),当读到EOF或换行符时,会结束读,如果读到换行符,会把\n写入str中
ㅤ fgets()有时会很有用,但有时候我们希望可以自己设置分隔符而不是使用换行符,可以通过fgetc实现
void my_fgets(char *str, size_t n, FILE* stream, char notify)
{
int c;
char *p;
p = str;
while(--n && (c=fgetc(stream)) != EOF && (*p++ = c) != notify );
if(c == notify)
*--p = '\0';
else
*p = '\0';
}
读二进制文件
fread
功能:
binary stream input
原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
ptr : 指向存放流的buf
size : 每次读多少个字节
nmemb : 读多少次
stream : 打开的文件指针
返回值:
成功 : 返回有效读取的次数(不是字节数)若出现错误或到达文件末尾,则可能小于nmemb。
若size或count为零,则fread返回零且不进行其他动作。
失败 : fread不区分文件尾和错误,因此调用者必须用feof和ferror才能判断发生了什么。
注意:
文件指针会移动
向流中写数据
写入单个字符
fputc
功能:
将一个字符c强制转换成unsigned char写入stream中
原型:
int fputc(int c, FILE *stream);
参数:
c : 字符
stream : 流
返回值:
成功:返回c
失败:返回EOF
if(fputc('p', stream) == EOF)
/* error */
写入字符串
fputs
功能:
将一个字符串写入stream中,不会写入结束标记符
原型:
int fputs(const char *s, FILE *stream);
参数:
s : 字符串
stream : 流
返回值:
成功:返回一个非负整数
失败:返回EOF
FILE *stream;
if(!stream)
/* error */
if(fputs("The ship is made of wood.\n", stream) == EOF)
/* error */
if(fclose(stream) == EOF)
/* error */
写入二进制数据
fwrite
功能:
写二进制数据
原型:
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
ptr : 存放数据的buf
size : 每次写多少个字节
nmemb : 写多少次
stream : 打开的文件指针
返回值:
返回有效写入的次数(不是字节数),如果该数字与 nmemb 参数不同,则会显示一个错误。
注意:
文件指针会移动
- fread_write.c
//在不同环境上,由于变量的长度、对齐等因素不同,可能另一个应用程序读取会出错
#include <stdio.h>
#include <time.h>
int main(int argc, char const *argv[])
{
struct myData
{
size_t no;
size_t size;
time_t lasttime;
}p1,p2 = {1, 20, time(NULL)};
FILE *in, *out;
out = fopen("./data","w");
if(!out){
perror("fopen");
return 1;
}
if(!fwrite(&p2, sizeof(p2), 1, out)){
perror("fwrite");
return 1;
}
if(fclose(out) == EOF){
perror("fclose");
return 1;
}
in = fopen("./data", "r");
if(!in){
perror("fopen");
return 1;
}
if(!fread(&p1, sizeof(p1), 1, in)){
perror("fread");
return 1;
}
if(fclose(in) == EOF){
perror("fclose");
return 1;
}
printf("No : %lu, size : %lu, lasttime : %lu\n",p1.no, p1.size, p1.lasttime);
return 0;
}
定位流
fseek
功能:
定位一个流
原型:
int fseek(FILE *stream, long offset, int whence);
参数:
stream : 流
offset : 偏移的数据量
whence :
SEEK_SET : 文件开头加上offset
SEEK_CUR : 当前位置加上offset
SEEK_END : 文件末尾加上offset
返回值:
成功 : 0,并清空文件结束标识符EOF
失败 : -1,设置errno
fsetpos
功能:
定位一个流
原型:
int fsetpos(FILE *stream, const fpos_t *pos);
参数:
stream : 流
pos : 文件指针要指向的位置
返回值:
成功: 0
失败: -1
== fseek(stream, offset, SEEK_SET)
rewind
功能:
重新设置流的初始位置
原型:
void rewind(FILE *stream);
参数:
stream :流
没有返回值
通过以下方式获取返回值
errno=0;
rewind(stream);
if(errno)
/* errno */
ftell
功能:
获取当前流位置
原型:
long ftell(FILE *stream);
参数:
stream :流
返回值:
成功:返回流的位置
失败: -1,errno
fgetpos
功能:
定位一个流
原型:
int fgetpos(FILE *stream, fpos_t *pos);
参数:
stream : 流
pos : 文件的位置
返回值:
成功: 0
失败: -1,errno
刷新流
ㅤ 对于 C 函数库中带"f"的 I/O 操作的缓冲区都是在用户空间,不是内核空间。也就是,这些调用的性能提升空间来自于用户空间,运行的是用户代码,而不是系统调用,只有当需要访问磁盘或其他某些介质时,才会发起系统调用。
ㅤ fflush 函数的功能只是把用户缓冲的数据写入到内核缓冲区。其执行结果看起来似乎没有用户缓冲区,而是直接调用 write 函数,fflush 函数并不保证数据最终写到物理介质上;为了确保数据最终会写到备份存储中,可以调用 fflush 后,立即调用 fsync
fflush
功能:
将 stream 指向的流中所有未写入的数据会被 flush 到内核中
原型:
int fflush(FILE *stream);
参数:
stream : 流
NULL 进程中所有打开的流都会被flush
返回值:
成功: 0
失败: EOF,errno
获取关联的文件描述符
fileno
功能:
获取关联的文件描述符
原型
int fileno(FILE *stream);
参数:
stream : 流
返回值:
成功: 返回指定stream关联的文件描述符
失败: -1
注意:
在操作和流关联的文件描述符之前,最好先对流进行刷新(flush),最好不要混合使用文件描述符和基于流的I/O操作
线程安全
ㅤ ㅤ在访问共享数据时,有两种方式可以避免它
- 采用同步数据访问机制(通过加锁实现)
- 把数据存储在线程的局部变量中
ㅤ 标准 I/O 函数在本质上是线程安全的。在每个函数的内部实现中,都关联了一把锁、一个锁计数器,以及持有该锁并打开一个流的线程,每个线程在执行任何 I/O 请求之前,必须首先获得锁而且持有该锁。两个或多个运行在同一个流上的线程不会交叉执行标准 I/O 操作,因此,在单个函数调用中,标准 I/O 操作是原子操作
ㅤ 在实际应用中,很多应用程序比独立函数调用级别更强的原子性。假设一个进程中有多个进程发起写请求,由于标准 I/O 函数是线程安全的,各个写请求不会交叉执行导致输出混乱。也就是说,即使两个线程同时发起请求操作,加锁可以保证一个写请求后再执行另一个写请求。但是,如果进程连续发起很多写请求,希望不但不同线程的写请求不会交叉,同一个线程写请求也不会交叉,需要通过标准 I/O 提供的一系列函数来实现
flockfile
功能:
加锁
原型:
void flockfile(FILE *filehandle);
参数:
filehandle : 文件流
返回值:
成功 : 0
失败 : 非0
funlockfile
功能:
解锁
原型:
void funlockfile(FILE *filehandle);
参数:
filehandle : 文件流
返回值:
成功 : 0
失败 : 非0
高级I/O
writev
功能:
write data into multiple buffers
原型:
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
参数:
fd : 文件描述符
iov :
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
iovcnt :
返回值:
成功 : 返回写的字节数,iovcnt*iov_len
失败 : -1,errno
注意:
1. 如果 iovcnt 个 iov_len 的和超出 SSIZE_MAX ,不会处理任何数据,返回 -1,errno EINVAL
2. iovcnt 值必须大于 0,且小于等于 IOV_MAX(1024);如果 iovcnt 为 0,该系统调用返回 0,count 大于 IOV_MAX,不会处理任何数据,返回- 1.error 设置位 EINVAL
3. 优化 iovcnt,Linux 内核必须分配内部数据结构来表示每个段。一般来说是基于 iovcnt的大小动态分配进行的。当 iovcnt 足够小的时候,内核会在栈上创建一个很小的段数组,避免动态内存分配,从而获得性能上的一些提示,iovcnt 的阈值一般设置为 8
4. writev 函数在处理下个缓冲区之前,会把当前缓冲区所有 iov_len 个字节数据输出
readv
功能:
read data into multiple buffers
原型:
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
参数:
fd : 文件描述符
iov :
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
iovcnt :
返回值:
成功 : 返回读的字节数,iovcnt*iov_len
失败 : -1,errno
注意:
readv 函数在处理下个缓冲区之前,会填满当前缓冲区的 iov_len 个字节
- writev.c
#include <stdio.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
int fd;
ssize_t nwr;
struct iovec iov[3];
fd = open("./hello.txt", O_WRONLY|O_CREAT|O_TRUNC);
if(fd < 0){
perror("open");
return 1;
}
/*char *buf[]={
"123.\n",
"123456.\n",
"123456789.\n"
};
for(int i=0; i<3; ++i){
iov[i].iov_base = buf[i];
iov[i].iov_len = strlen(buf[i]) + 1;
printf("len : %lu", iov[i].iov_len);
} */
char buf[3][40]={
"123.\n",
"123456.\n",
"123456789.\n"
};
for(int i=0; i<3; ++i){
iov[i].iov_base = buf[i];
iov[i].iov_len = sizeof(buf[i]);
}
nwr = writev( fd, iov, 3);
if(nwr == -1){
perror("writev");
return 1;
}
printf("wrote %lu bytes\n",nwr);
if(close(fd)){
perror("close");
return 1;
}
return 0;
}
- readv.c
#include <stdio.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
int fd;
ssize_t nrd;
struct iovec iov[3];
fd = open("./hello.txt", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
char buf[3][40] = {0};
for(int i=0; i<3; ++i){
iov[i].iov_base = buf[i];
iov[i].iov_len = sizeof(buf[i]);
}
nrd = readv( fd, iov, 3);
if(nrd == -1){
perror("readv");
return 1;
}
printf("nrd : %lu\n", nrd);
for(int i=0; i<3; ++i)
printf("%s", buf[i]);
if(close(fd)){
perror("close");
return 1;
}
return 0;
}
EPOLL
ㅤ 对于 poll 和 select,每次调用时都需要所有被监听的文件描述符列表。内核必须遍历所有被监视的文件描述符。当这个文件描述符列表变得很大时,每次调用都要遍历列表就成了规模上的瓶颈。
epoll_create1
功能:
创建一个新的epoll实例
原型:
int epoll_create1(int flags);
参数:
flags :
0 : == epoll_create
EPOLL_CLOEXEC : 进程被替换时关闭文件描述符、
返回值:
成功:返回和该实例关联的文件描述符,这个文件描述符和真正的文件没有关系,仅仅是为了后续调用epoll而创建的
失败:-1,errno
epoll_create
功能:
创建一个新的epoll实例
原型:
int epoll_create(int size);
参数:
size : 告诉内核这个监听数目一共有多大
自从Linux 2.6.8开始,size参数被忽略,但是依然要大于0。
返回值:
成功:返回和该实例关联的文件描述符,这个文件描述符和真正的文件没有关系,仅仅是为了后续调用epoll而创建的
失败:-1,errno
注意:
当创建好 epoll 句柄后,它就是会占用一个fd值,必须调用 close() 关闭,否则可能导致 fd 被耗尽
epoll_ctl
功能:
control interface for an epoll descriptor
原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
epfd : epoll_create 或 epoll_create1 的返回值
op:
EPOLL_CTL_ADD : 把文件描述符fd添加到epfd指定epoll监听实例集中,监听event中定义的事件
EPOLL_CTL_DEL : 把文件描述符fd从epfd中移除
EPOLL_CTL_MOD : 使用event指定的更新事件修改已有fd上的监听行为
fd : 文件描述符
event :
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
返回值:
成功 : 0
失败 : -1 errno
EPOLL事件 | 说明 |
---|---|
EPOLLIN | 关联的文件可用于 read(2)操作 |
EPOLLOUT | 关联的文件可用于 write(2)操作 |
EPOLLRDHUP | 流套接字对等方关闭连接,或关闭写入一半连接。 (此标志对于编写简单的代码以使用边缘触发监控来检测对等设备关闭特别有用。) |
EPOLLPRI | 有紧急数据可用于read(2)操作。 |
EPOLLERR | 在关联的文件描述符上发生错误。epoll_wait(2)将始终等待此事件;无需在事件中进行设置。 |
EPOLLHUP | 挂断发生在关联的文件描述符上。 epoll_wait(2)将始终等待此事件; 无需在事件中进行设置。 请注意,当从通道(例如管道或流套接字)读取时,此事件仅指示对等方关闭了其通道的末端。 仅在使用完该通道中的所有未完成数据之后,从该通道进行的后续读取将返回0(文件末尾)。 |
EPOLLET | 为关联的文件描述符设置边缘触发的行为。 epoll的默认行为是“级别触发”。 |
EPOLLONESHOT | 设置关联文件描述符的一次性行为。 这意味着在使用epoll_wait(2)提取事件后,内部会禁用关联的文件描述符,并且epoll接口不会报告其他事件。 用户必须使用EPOLL_CTL_MOD调用epoll_ctl()才能使用新的事件掩码重新配置文件描述符。 |
EPOLLWAKEUP | 如果清除了EPOLLONESHOT和EPOLLET并且该进程具有CAP_BLOCK_SUSPEND功能,请确保在此事件挂起或正在处理时,系统不会输入“挂起”或“休眠”。 从该事件通过调用epoll_wait(2)返回该事件直到对同一epoll(7)文件描述符进行下一次对epoll_wait(2)的调用,即关闭该文件描述符,该事件被视为“已处理” 使用EPOLL_CTL_DEL删除事件文件描述符,或使用EPOLL_CTL_MOD清除事件文件描述符的EPOLLWAKEUP。 |
- 添加
struct epoll_event event;
int ret;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLOUT;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
if(ret)
perror("epoll_ctl");
- 修改
struct epoll_event event;
int ret;
event.data.fd = fd;
event.events = EPOLLIN ;
ret = epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &event);
if(ret)
perror("epoll_ctl");
- 删除
struct epoll_event event;
int ret;
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
if(ret)
perror("epoll_ctl");
epoll_wait
功能:
wait for an I/O event on an epoll file descriptor
原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数:
epfd :epoll_create 或 epoll_create1 的返回值
events : 指向描述每个事件的 epoll_event 结构体
maxevents: 最多可以有 maxevents 事件
timeout : 阻塞时间(毫秒数)
文件描述符有事件发生
被信号处理程序中断
超时到期
返回值:
成功 : events指向描述每个事件的epoll_event结构体的内存,且最多可以有maxevents个事件,返回值是事件数
失败 : -1,errno
#define MAX_EVENTS 64
struct epoll_event *events;
int nr_events,i,epfd;
events = malloc(sizeof(struct epoll_event) * MAX_EVENTS));
if(!events){
perror("malloc");
return 1;
}
nr_events = epoll_wait(epfd, events, MAX_EVENTS, -1);
if(nr_events < 0){
perror("epoll_wait");
free(events);
return 1;
}
for(i=0; i<nr_events; ++i){
printf("event=%ld on fd = %d\n",events[i].events,events[i].data.fd);
}
文件查看命令
od
# 使用单字节八进制解释进行输出,注意左侧的默认地址格式为八字节
od -c 文件
# 使用ASCII码进行输出,注意其中包括转义字符
od -t d1
# 使用单字节十进制进行解释
od -A d -c
# 设置地址格式为十进制
od -A x -c
# 设置地址格式为十六进制
od -j 2 -c
du
du -h 文件
目录与链接
opendir
功能说明:
打开一个目录
原型:
DIR *opendir(const char *name);
参数:
name : 文件的路劲名
返回值:
打开成功,返回一个目录指针
打开失败,则返回0
readdir
功能说明:
访问指定目录中下一个连接的细节
原型:
struct dirent *readdir(DIR *dirp);
参数:
dirp : 目录指针
返回值:
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* not an offset; see NOTES */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported by all filesystem types */
char d_name[256]; /* filename */
};
返回一个指向 dirent 结构的指针,它包含指定目录中下一个连接的细节;
没有更多连接时,返回 0
closedir
功能说明:
关闭一个已经打开的目录
原型:
int closedir(DIR *dirp);
参数:
dirp : 目录指针
返回值:
成功 : 0
失败 : -1
- myls.c
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
void listfile(char name[])
{
struct dirent *direntpointer;
DIR *direct;
if ((direct = opendir(name)) == NULL)
{
fprintf(stderr, "myls: can't open %s\n", name);
return;
}
else
{
while ((direntpointer = readdir(direct)) != NULL)
printf("%s\n", direntpointer->d_name);
closedir(direct);
}
}
int main(int argc, char *argv[])
{
if (argc == 1)
{
listfile(".");
}
else
while (--argc > 0)
{
printf("%s\n", *++argv);
listfile(*argv);
}
return 0;
}
mkdir
功能说明:
用来创建一个称为 pathname 的新目录,它的权限位设置为 mode
原型:
int mkdir(const char *pathname, mode_t mode);
参数:
pathname : 文件的路径名
mode : 权限位
返回值:
调用 : 0
失败 : -1
rmdir
功能说明:
删除一个空目录
原型:
int rmdir(const char *pathname);
参数:
pathname : 文件的路径名
返回值:
调用 : 0
失败 : -1