Linux系统下I/O操作讲解

描述

Linux系统下I/O

一、I/O简介

I/O(输入/输出)是在主存和外部设备(磁盘驱动器、网络、终端)之间复制数据的过程。输入是从外部设备复制到主存,输出是从主存复制到外部设备。

在Linux系统中所有的I/O设备都被映射称为文件,所有的输入输出都被当做相应文件的读和写来执行,所以内核提供了系统级的I/O函数接口,使得所有输入输出都以统一且一致的方式来执行。

  1. 打开文件,返回一个非负整数,叫做描述符
  2. 每个进程都默认打开三个描述符,标准输入 STDIN_FILENO(描述符0)、标准输出 STDOUT_FILENO(描述符1)、标准出错 STDERR_FILENO(描述符2)。
  3. 读写文件,读就是从文件复制n个字节到内存,写就是从内存复制n个字节到文件。
  4. 文件偏移:默认打开文件是从文件开头起始的字节偏移量,可以使用seek来操作。
  5. 关闭文件。

今天从四个方面来说I/O,文件I/O、标准I/O库、高级I/O、终端I/O。

  1. 文件I/O: 文件的打卡、读写、关闭、偏移。
  2. 标准I/O库:Linux提供的标准I/O库函数
  3. 高级I/O:非阻塞I/O、I/O多路转接、异步I/O
  4. 终端I/O: 更改终端属性操作的函数

二、文件I/O

Linux系统中文件I/O一般只用到以下五个函数:open、read、write、lseek、close。每次read、write都是一次系统调用(从用户层拷贝到内核层再拷贝到用户层)且不带缓冲。

  1. 文件描述符

对于内核而言,每个打开的文件都是通过文件描述符引用的,每个文件描述符都是一个非负整数,打开或者创建一个文件都会返回一个文件描述符,通过这个文件描述符来进行读写,

  1. 打开/创建文件
#include
#include
#include

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:创建或者打开一个文件,返回一个文件描述符
参数: pathname:路径名/文件名
flags:标志位
O_RDONLY 只读

O_WRONLY 只写

O_RDWR 既可以读也可以写

O_APPEND 以追加的方式操作文件

O_CREAT 如果文件不存在,则创建

O_TRUNC 如果文件存在,则清空文件的数据

O_EXCL 表示文件已经存在,而又重复创建一次,open函数会返回错误,
返回文件已经存在的错误,对错误做处理之后,直接打开文件就可以

O_APPEND 从文件末尾位置追加写入

O_SYNC 每次write等物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O,(后边会用到)

O_RSYNC 每个以文件描述符作为参数进行的read操作等待,直到所有对文件同一部分挂起的写操作都完成。

mode:如果是创建一个文件,需要添加对应文件的属性,模式属性一般用一个八进制数代替,如果属性成立,为1,不成立,则为0
rwxr-x-wx --> 0753
rw-rw-r-- --> 0664

返回值:成功:文件描述符
失败:-1
  1. 关闭文件

关闭一个文件时会自动释放加在该文件上的所有锁,当进程终止时会自动关闭所有打开的文件。

#include

int close(int fd);
参数:fd:open返回的文件描述符
返回值:成功 0, 失败 -1.
  1. 文件偏移

通常所有读写操作都是从当前文件偏移量处开始,并使偏移量增加读写的字节数,默认是0。可以使用lseek显式打开文件设置偏移量。

#include
#include

off_t lseek(int fd, off_t offset, int whence);
参数:fd : open函数打开的文件
offset: 与whence有关
whence: 基准点
SEEK_SET 将读写位置指向文件头后再增加offset个位移量。
SEEK_CUR 以目前的读写位置往后增加offset个位移量。
SEEK_END 将读写位置指向文件尾后再增加offset个位移量(使用该参数可以算出文件字节数)
当whence 值为SEEK_CUR 或SEEK_END时,参数offet允许负值的出现。
返回值:成功,返回文件偏移量,失败 -1.
注释:文件偏移量可以大于文件长度,这样就会构成空洞文件,对于多出的这些字节被读出为0.空洞文件在磁盘中不占用存储区。
  1. 读文件
#include

ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符
buf:读取到的数据
const:每一次最多读取到的字节数
返回值:
成功:读取的字节数 如果是0 代表结尾
失败:-1
  1. 写文件
#include

ssize_t write(int fd, const void *buf, size_t count);
功能:向一个文件描述符写数据
参数:
fd:文件描述符
buf:要写入的数据
const:每一次最多写入到的字节数
返回值:
成功:写入的字节个数
失败:-1
失败原因多是磁盘已满或者超过一个给定进程的文件长度限制。
  1. 文件共享

Linux系统支持不同进程间共享打开文件,在此先说一下内核用于所以I/O的数据结构。

内核使用三种数据结构表示打开的文件,他们之间的关系决定了文件共享中一个进程对另一个进程的影响。 首先每个进程在进程表中有一个记录项,每个记录项包含一张打开的文件描述符,每个描述符占用一项,与文件描述符有关的是:

1.文件描述符标志

2.指向文件表项的指针

其次内核为每个打开文件维持一张文件表,文件表项包含:

1.文件状态标志(读、写、阻塞等)

2.当前文件偏移量

3.指向该文件v节点表项指针     最后每个打开文件(设备)都有一个v节点结构,它包含了:

     1.文件类型

     2.对该文件进行各种操作的指针。

     3.i节点(i-node),包含了文件的长度、所以者、指向文件实际数据块在磁盘的位置。

这些信息都是在打开文件时候从磁盘拷贝到内存,所以这些信息都是随时可用的。总结一下这三张表关系

进程表项: fd标志
文件指针(文件表项):文件状态标志
当前文件偏移量
v节点指针(v节点表项): v节点信息
v_data: i节点(i节点表项): i节点信息
当前文件长度

了解了内核的这三个数据结构之后我们回过头来看文件共享。

假定一个进程打开了一个文件,返回文件描述符是4,另一个进程也打开了这个文件描述符返回的文件描述符是5,打开该文件的每个进程都有一个文件表项(进程对该文件的当前偏移量),但是该文件只有一个v节点。

  1. 每当write之后,文件表项中担负起偏移量会增加写入的字节数,如果当前文件偏移量超出了当前文件长度则i节点表项中文件长度也增加。
  2. 如果使用O_APPEND打开一个文件,相应的标志被设置到文件表项中的文件状态标志,每次对该文件写操作时,文件表项中当前文件偏移量会被设置为i节点表项的文件长度。
  3. 当使用lseek函数定位到文件尾端时候,文件表项中的当前文件偏移量被设置为i节点表项中的文件长度。
  4. 存在多个文件描述符指向同一个文件的情况。

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

驱动器

  1. 原子操作

当有多个进程操作一个文件时候为了数据同步Linux系统提供了原子操作。

1.open一个文件时候使用 O_APPEND 标志

2.使用pread 和 pwrite 函数 pread/pwrite 相当于调用lseek之后调用read/write,但是区别在于调用pread/pwrite时,无法中断其定位和读写操作,而且不更新当前文件的偏移量。

#include

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
功能:读文件
参数:fd:文件描述符
buf:读缓冲区
count:缓冲区大小
offset:偏移量
返回值: 成功:读到的字节数,如果读到文件尾返回0, 失败-1

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
功能:写文件
参数:fd:文件描述符
buf:写缓冲区
count:缓冲区大小
offset:偏移量
返回值: 成功:读到的字节数, 失败-1
  1. 将缓冲区数据写到磁盘

在传统Unix系统实现中大多数磁盘I/O通过缓冲区进行的,当我们向文件写数据时,内核通常将数据复制到缓冲区中,之后再写到磁盘,这种方式称为延迟写。下面函数将缓冲区数据写入到磁盘。

#include

void sync(void);
将修改过的块缓冲区排队写到队列就返回,数据并不一定写入到磁盘。命令sync就是调用sync函数。update系统守护进程每30s调用一次该函数。

int fsync(int fd);
只对一个文件描述符其作用,并且磁盘操作结束后才返回。

int fdatasync(int fd);
等同于fsync,但是同时更新文件属性。
  1. 修改已打开的文件属性
#include
#include

int fcntl(int fd, int cmd, ... /* arg */ );
功能:修改已打开文件属性
参数:fd:文件描述符
cmd: F_DUPFD:复制文件描述符,新的文件描述符作为返回值返回。新文件描述符与旧fd共享同一文件表项,但是有自己的文件描述符标志,其FD_CLOEXXEC文件描述符标志被取消
F_DUPFD_CLOEXEC:复制文件描述符,设置与新文件描述符关联的FD_CLOEXXEC文件描述符标志的值,返回新文件描述符
F_GETFD:对应于fd的文件描述符标志作为函数返回值
F_SETFD:对应fd设置文件描述符标志,新值为第三参数值
F_GETFL:对应fd的文件状态标志作为函数返回值
F_SETFL:将文件状态标志设置为第三个参数的值
F_GETOWN:获取当前SIGIO和SIGURG信号的进程ID和组ID
F_SETOWN:设置接收SIGIO和SIGURG信号的进程ID和组ID
第三参数:总是一个整数,一般0
返回值:出错:-1
成功:其他
  1. ioctl 函数

ioctl函数是I/O操作的万金油,内核对设备的IO通道控制操作函数,多用于驱动程序。

#include

int ioctl(int fd, int request, ...);
参数:@fd :文件描述符的序号
@request :请求 代表不同操作的数字值
@... :可变参数,(写或者不写根据请求决定)
:传递的是整数,或者地址
返回值:出错:-1
成功:其他

ioctl函数的实现需要一种命令码
32位
比特位 含义
31 - 30 00 : 命令不带参数
01 : 命令从驱动中获取数据,读方向
10 : 命令把数据写入驱动,写方向
11 : 命令即写又读:双向
29 - 16 类型的大小
15 - 8 类型
7 - 0 序号

三 、标准I/O库

标志I/O库处理了很多细节,比如缓冲区的分配、优化块长度执行I/O等,更方便大家进行I/O操作

在前面说的I/O函数都是围绕着文件描述符进行操作的,在标准I/O库里对应的是 流 进行操作的,当打开一个一个流时,标准I/O库函数fopen返回一个指向FILE对象的指针。它是一个结构体包含了标准I/O库

所管理该流的所有信息,包括用于实际I/O的文件描述符、指向用于该流的缓冲区指针、缓冲区长度、以及当前缓冲区中的字符等。

对应文件描述符每个进程定义了三个流,标准输入(stdin)、标准输出(stdout)、标准出错(stderr)

2.缓冲区

标准I/O库提供缓冲区的目的是为了尽可能减少使用read和write(太消耗资源了),它对每个I/O流自动地进行缓冲管理,库函数提供的接口,在内存中创建一块缓冲区,直到满足一定条件,才会真正写入,本质上还是系统调用,可以在不同系统间进行数据传输。

有以下三种缓冲

1.全缓冲,操作的文件,3个条件:

  1. 缓冲区满,则会刷新缓冲区 4096byte
  2. 程序正常结束
  3. fflush刷新缓冲区(将内容写到磁盘,在驱动程序表示丢弃缓冲区数据)

2.行缓冲:指针对终端进行操作,4个条件:

  1. 缓冲区满,则会刷新缓冲区 1024byte
  2. 程序正常结束
  3. fflush刷新缓冲区
  4. “n”

3.无缓冲:指针终端进行操作

修改系统默认缓冲(一定要在流打开之后修改)

#include

void setbuf(FILE *stream, char *buf);
功能:打开或者关闭缓冲机制
参数:stream:打开的流
buf:指向一个长度为BUFSIZE的缓冲区,设置为null则关闭缓冲
返回值:成功0,失败非0

int setvbuf(FILE *stream, char *buf, int mode, size_t size);
功能:打开或者关闭缓冲机制
参数:stream:打开的流
buf:指向一个长度为BUFSIZE的缓冲区,设置为null则系统自动分配
mode:_IONBF :无缓冲,此选项可以忽略buf和size
_IOLBF :行缓冲
_IOFBF :全缓冲
返回值:成功0,失败非0

刷新缓冲区,将所有未写的数据传输到内核。如果stream为null,则刷新所有缓冲区。

#include

int fflush(FILE *stream);
  1. 打开流

打开一个流默认是全缓冲,当打开终端设备时候默认为行缓冲。

#include

FILE *fopen(const char *path, const char *mode);
功能:打开一个标准I/O流
参数:path:文件名
mode:打开模式 (b:二进制文件)
r/rb:打开文件对文件进行读操作,文件必须存在,
r+/r+b/rb+:打开文件对文件进行读写操作,文件必须存在
w/wb:打开或者创建文件,对文件进行写入
w+/w+b/wb+:打开或者创建文件,对文件进行读写操作
a/ab:打开或者创建文件,从文件末尾位置追加数据(多个进程追加一个文件也可以正确写入)
a+/a+b/ab+:打开或者创建文件,从文件末尾进行读取、追加文件。如果文件不存在创建文件,从文件起始处读写。

返回值:成功返回文件指针,失败返回null

FILE *fdopen(int fd, const char *mode);
功能:取一个文件描述符,并使标准I/O流与之相关联,此函数常用于由创建管道和网络通信管道函数返回的描述符。因为这些特色文件不能用fopen打开。
返回值:成功返回文件指针,失败返回null

FILE *freopen(const char *path, const char *mode, FILE *stream);
功能:在一个指定流上打开一个文件,如果已经打开则先关闭再打开,此函数一般用于将一个文件打开为一个预定义的流:stdin、stdout、stderr
参数:path:文件名
mode:
返回值:成功返回文件指针,失败返回null
  1. 关闭流

当关闭一个流时候,缓冲区所有数据都被丢弃。

#include

int fclose(FILE *fp);
返回值:成功0,失败EOF(-1)
  1. 读流和写流

每次打开一个I/O可以使用三种不同方式进程读写流 1. 每次读写一个字符的I/O 2. 每次读写一行的I/O,没次以换行符终止 3. 直接I/O,直接读写某种指定长度的对象,常用于二进制和结构体读写。

读写一个字符

#include

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
功能:读取数据
参数:流
返回值: 成功 读取的字符,失败 -1(EOF)
区别:getc为宏。fgetc为函数,所以fgetc可以当做地址作为参数传递,getc不可以。

#include

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
功能:写入文件数据
参数:c 写入的字符 stream 流
返回值:成功 写入的字符,失败 EOF
  1. 读写一行字符
#include
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);(不推荐使用,因为无法指定长度,可以造成缓冲区溢出)
功能:读取文件中的一行字符,遇到n 结束
参数:s 指向用户开辟的缓冲区,实现定义一个数组
size:要求读取字节个数
stream:流
返回值:成功 读取的字符串,失败 EOF;

#include

int fputs(const char *s, FILE *stream);
int puts(const char *s);
功能:输出以null结尾的字符串数据数据到指定文件中
参数:s 指定要被读取数据的缓冲区
输出 n 但不能输出�
返回值:成功 读取的字符串,失败 EOF;
  1. 二进制I/O读写
#include
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:读文件
参数:ptr:事先定义的变量,需要传递变量的
size:每个对象的大小
number:对象个数
stream:流
返回值:成功:返回实际读取到对象的个数
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);
功能:写文件
参数:ptr:事先定义的变量,需要传递变量的
size:每个对象的大小
number:对象个数
stream:流
返回值:成功:返回实际写对象的个数
注释:这两个函数存在一个问题就是只能读写同一系统上的数据,如果是不同系统则会造成问题。因为在不同系统同一结构的同一成员偏移量可能不同。
  1. 定位流
#include

int fseek(FILE *stream, long offset, int whence);
功能:文件定位
参数:stream 流
offset:偏移量
whence:基准点
SEEK_SET 文件开头位置
SEEK_CUR 文件当前位置
SEEK_END 文件末尾位置
从后往前偏移加 - 号
返回值:成功 0 失败 -1

long ftell(FILE *stream);
功能 返回当前文件位置指针的位置是在那个地址,使用数字的形式表示
参数:stream 流
返回值:成功返回文件当前位置,出错-1.

void rewind(FILE *stream);
参数:stream 流
功能: 把文件指针指向开头

int fgetpos(FILE *stream, fpos_t *pos);
功能:将文件位置指示器的当前值存入pos指向的对象中

int fsetpos(FILE *stream, fpos_t *pos);
功能:将文件位置定位到pos指示的值位置。
  1. 格式化I/O
格式化输出

#include

int printf(const char *format, ...);
功能:发送格式化输出到标准输出 stdout。
参数:format -- 这是字符串,包含了要被写入到标准输出 stdout 的文本

int fprintf(FILE *stream, const char *format, ...);
功能:写入到指定的流。

int sprintf(char *str, const char *format, ...);
功能:将格式化字符串写入到str中,自动会加一个null字节
参数:str:保存格式化的字符串
format -- 这是字符串
返回值:成功:返回写入到str中字符数(不包含null),失败负数

int snprintf(char *str, size_t size, const char *format, ...);
功能:同sprintf,但是sprintf可能会造成缓冲区溢出功能,所以snprintf会限定写入字节数。
参数:str:保存格式化的字符串,自动会加一个null字节
size:字符串大小
format -- 这是字符串
返回值:如果格式化后的字符串长度小于等于 size,则会把字符串全部复制到 str 中,并给其后添加一个字符串结束符 �;
如果格式化后的字符串长度大于 size,超过 size 的部分会被截断,只将其中的 (size-1) 个字符复制到 str 中,并给其后添加一个字符串结束符 �,返回值为欲写入的字符串长度。
失败:负数
格式字符: %h:输出short型
%d 十进制有符号整数
%md:m为指定的输出字段的宽度。如果数据的位数小于m,则左端补以空格,若大于m,则按实际位数输出。
%ld:输出长整型数据。
%lld: long long型
%u 十进制无符号整数
%f 浮点数 输出float
%lf 浮点数 输出double
%m.nf:输出共占m列,其中有n位小数,如数值宽度小于m左端补空格。
%-m.nf:输出共占m列,其中有n位小数,如数值宽度小于m右端补空格。
%s 字符串
%c 单个字符
%p 指针的值
%% 百分号本身
%e 指数形式的浮点数
%x, %X 无符号以十六进制表示的整数
%o 无符号以八进制表示的整数
%g(%G) 浮点数不显无意义的零"0"
%p 输出地址符
%lu 32位无符号整数
%llu 64位无符号整数
附加格式说明符
m 输出数据域宽,数据长度 .n 对实数,指定小数点后位数(四舍五入)
- 输出数据在域内左对齐(缺省右对齐)

+ 指定在有符号数的正数前显示正号(+)
0 输出数值时指定左面不使用的空位置自动填0
# 在八进制和十六进制数前显示前导0,0x
l long类型输出 %ld
double类型输出 %lf

格式化输入:
#include

int scanf(const char *format, ...);
功能:按照格式从终端输入数据
参数:
format:格式控制串
%d 十进制整数
%c 字符数据
%s 字符串
%f 浮点类型

arg:可变参
如果要将输入的数据保存在arg变量里面,需要传arg的地址
返回值:成功:输入的个数 失败EOF

int fscanf(FILE *stream, const char *format, ...);
功能:从流 stream 读取格式化输入
参数:stream :这是指向 FILE 对象的指针,该 FILE 对象标识了流。
format :这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符 和 format 说明符。
返回值:如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。

int sscanf(const char *str, const char *format, ...);
功能:从字符串读取格式化输入。
参数:str:这是 C 字符串,是函数检索数据的源。
format :这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符 和 format 说明符
返回值:如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。

格式字符:同格式化输出,左补空格;否则按实际输出
  1. 临时文件
#include

char *tmpnam(char *s);
功能:产生一个与现有文件不同名的文件,每次调用都会产生不同路径的临时文件
参数:保存返回的路径名
返回值:返回文件路径名

#include

FILE *tmpfile(void);
功能:产生一个临时二进制文件(wb+),关闭该文件时会自动删除该文件
参数:保存返回的路径名
返回值:返回文件路径名

11 内存流

标准I/O库都是是将文件中数据取出来缓冲在内存中,现在我们可以直接通过缓冲区与主存直接来回传递数据,不依赖文件。仍然使用FILE指针,这些流看起来像文件流,其实是内存流。

内存流不访问文件只访问主存,所以如果标准I/O流作为参数用于临时文件的话,用内存流替代会有很大性能提高。

#include

FILE *fmemopen(void *buf, size_t size, const char *mode);
功能:内存流创建
参数:buf:指向缓冲区的开始位置,如果为null,读写都没有任何意义。
size:指定缓冲区大小的字节数,如果buf为null,则自动分配大小
mode:同fopen的mode
返回值:成功 返回流指针,失败null

FILE *open_memstream(char **ptr, size_t *sizeloc);
功能:创建流面向字节

#include

FILE *open_wmemstream(wchar_t **ptr, size_t *sizeloc);
功能:创建流面向宽字节

四、高级I/O

非阻塞I/O、I/O多路转接、异步I/O、记录锁,这些都会在进程间通信用到

  1. 非阻塞I/O

对于给定的文件描述符,有两种方法指定为非阻塞I/O。

  1. 调用open获得描述符时候指定 O_NONBLOCK标志
  2. 对于打开的文件描述符,调用fcntl函数,将O_NONBLOCK标志打开
  3. 记录锁

记录锁:当一个进程正在读或者写一个文件某部分的时候,使用记录锁可以阻止其他进程修改同一文件区。

int fcntl(int fd, int cmd, ... /* arg */ );
对于记录锁,cmd的参数为 F_GETKL、F_SETLK、F_SETLKW。第三个参数为指向flock结构的指针
struct flock {
short l_type; 锁的类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁)
short l_whence; SEEK_CUR、SEEK_SET、SEEK_END
off_t l_start; 加锁或者解锁的区域起始偏移量
off_t l_len; 区域长度
pid_t l_pid; 持有锁阻塞当前的进程
};
如果len为0,表示锁的范围无限大,不管向文件追加多少数据都在锁范围内。
对整个文件加锁,len=0,whence=EEK_SET。
共享读锁:任意多个进程可以在给定字节上有一把共享读锁,
独占性写锁:如果给定字节已经有写锁,那么不可再加任何锁。
F_GETKL:判断由flock结构的指针所描述的锁是否会被另外一把锁排斥。如果存在一把锁,它阻止创建由flock结构的指针所描述的锁,如果不存在则吧type修改为F_UNLCK
F_SETLK:由flock结构的指针所描述的锁,如果试图获取一把锁,系统阻止给我们锁则返回错误
F_SETLKW:如果请求锁,因为其他进程在使用,则调用进程进入休眠,直到锁可用被唤醒。

当一个进程终止时候,它所建立的所有锁都会释放,同样关闭一个文件描述符,与该文件描述符相关的锁都会释放。
fork产生的子进程不继承父进程设置的锁。
  1. I/O多路转接

1.对于从一个文件描述符读,然后又写另一个文件描述符这样的操作,我们通常这样写

while(read(fd,buf,size)) {
write(fd,buf,size);
}

这种阻塞I/O操作,我们经常见,也是最低级的写法,因为可能因为读阻塞导致写阻塞。这时候我们使用异步I/O,进程告诉内核,当描述符准备好时候通过信号通知内核,但是他也有限制,只有在描述符是

网络或者终端设备时候才会起作用。

2.IO多路复用基本思想

先构造一张有关描述符的表,然后调用一个函数,当这些文件描述符中的一个或多个已准备好进行IO时函数才返回,函数返回时告诉进程已经有描述符就绪,可以进行IO操作。

3.实现函数select

select函数可以使我们执行I/O多路转接,通过传给select函数的参数可以告诉内核:

a.我们所关心的描述符

b.对于每个描述符我们所关心的条件,是否想从一个给定描述符读/写,是否关心描述符异常

c.愿意等待多长时间

也可以通过返回值得到以下信息

a.已经准备好的文件描述符

b. 对于读、写、异常者三个条件中每一个,哪些已经准备好

然后我们就可以使用read和write函数读写。

#include
#include
#include

int select(int nfds,fd_set *read_fds,fd_set *write_fds,fd_set *except_fds,struct timeval *timeout);
参数: nfds 所有监控文件描述符最大的那一个 +1.(因为文件描述符编号从0开始,所以要加1)
read_fds 所有可读的文件描述符集合。 没有则为NULL
write_fds 所有可写的文件描述符集合。 没有则为NULL
except_fds 处于异常条件的文件描述符 没有则为NULL
timeval: 超时设置。 NULL:一直阻塞,直到有文件描述符就绪或出错
0 :仅仅监测文件描述符集的状态,然后立即返回
非0 :在指定时间内,如果没有事件发生,则超时返回
返回值:当timeval设置为NULL:返回值 -1 表示出错
>0 表示集合中有多少个描述符准备好
当设置timeval非0时: 返回值 -1:表示出错
>0: 表示集合中有多少描述符准备好
=0: 表示时间到了还没有描述符准备好

对于fd_set数据类型有以下四种处理方式 fd:文件描述符、 fdset文件描述符集合
void FD_SET(int fd,fd_set *fdset): 将fd加入到fdest
void FD_CLR(int fd,fd_set *fdest): 将fd从fdest里面清除
void FD_ZERO(fd_set *fdest): 从fdest中清除所有文件描述符
void FD_ISSET(int fd,fd_set *fdest):判断fd是否在fdest集合中
这些接口实现为宏或者函数,调用 FD_ZERO 将fd_set变量的所有位置设置为0,如果要开启描述符集合的某一位,可以调用 FD_SET ,调用FD_CLR 可以清除某一位,FD_ISSET用来检测某一位是否打开。
在申明了一个描述符集合之后,必须使用FD_ZERO将其清零,下面是使用操作:
fd_set reset;
int fd;
FD_ZERO(&reset);
FD_SET(fd, &reset);
FD_ZERO(STDIN_FILENO, &reset);
if (FD_ISSET(fd, &reset)) {}

对于“准备好” 这个词这里说明一下,什么才是准备好,什么是没有准备好,如果对读集(read_fds/write_fds) 中的一个描述符进行read/write操作没有阻塞则认为是准备好,或者对except_fds有一个未决异常条件,则认为准备好。
一个描述符的阻塞并不影响整个select的阻塞。当文件描述符读到文件结尾时候,read返回0.

4.实现函数poll

poll函数与select函数相似,不同的是,poll不是为每个条件(读、写、异常)构造一个文件描述符,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号,poll函数可以用于任何类型的文件描述符。

#include

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:fds:pollfd结构数组
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求事件 */
short revents; /* 返回事件 */
};
events:需要将events设置为以下一个或者多个值,这些值会告诉内核哪些是我们关系的文件描述符
POLLIN 不阻塞地读高优先级数据意外的数据
POLLRDNORM 不阻塞地读普通数据
POLLRDBAND 不阻塞地读优先级数据
POLLPRI 不阻塞地读高优先级数据
POLLOUT 普不阻塞地读写普通数据
POLLWRNORM 同POLLOUT
POLLWRBAND 不阻塞地写低优先级数据
POLLERR 发生错误
POLLHUP 发生挂起(当挂起后就不可以再写该描述符,但是可以读)
POLLNVAL 描述字不是一个打开的文件
revents:返回的文件描述符,用于说明描述符发生了哪些事件。
nfds:数组中元素数
timeout:等待时间
= -1:永远等待,直到有一个描述符准备好,或者捕捉到一个信号,如果捕捉到信号返回-1。
= 0 :不等待,立即返回。这是轮询的方法。
> 0: 等待的毫秒数,有文件描述符准备好或者timeout超时立即返回。超时返回值为0.

5.散布读和聚集写

就是在一次函数调用中读、写多个非连续的缓冲区。

#include

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
功能:散布读
参数:fd:文件描述符
iov:iovec结构指针
struct iovec {
void *iov_base; 缓冲地址
size_t iov_len; 缓冲大小
};
iovcnt:iov数组元素个数
返回值:成功:已读个数,失败:-1

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
功能:聚集写
参数:fd:文件描述符
iov:iovec结构指针
struct iovec {
void *iov_base; 缓冲地址
size_t iov_len; 缓冲大小
};
iovcnt:iov数组元素个数
返回值:成功:已写个数,失败:-1

6.存储映射I/O

存储映射I/O,将一个磁盘文件映射到内存中的一个缓冲区上,从这个缓冲区读写数据就相当于读写文件数据,就可以不再使用read、write。

#include
void *mmap(void *addr,size_t len,int prot,int flags,int fd,off_t offset);
功能:将文件或设备空间映射到共享内存区,因此当从共享内存读数据时就相当于从文件中读取数据
参数: addr:要映射的起始地址,通常为NULL,让内核自动分配
len:映射到进程地址空间的字节数
port:映射区保护方式 PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROC_EXEC 映射区可执行
PROC_NONE 映射区不可访问

flags: MAP_SHARED 变动是共享的
MAP_PRIVATE 变动是私有的
MAP_FIXED 准确解释addr参数, 如果不指定该参数, 则会以4K大小的内存进行对齐
MAP_ANONYMOUS 建立匿名映射区, 不涉及文件
fd: 文件描述符,使用前必须先打开文件。
offset:从文件头开始偏移量为0

p=(STU*)mmap(NULL,sizeof(STU)*5,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0) //STU* 数据类型

五、终端I/O

终端I/O系统是一个非常复杂的东西,我们不会去讲解它,但是它有几个非常重要的函数需要我们学,这几个函数用来去嵌入式的串口编程

  1. 获取/设置终端参数
#include
#include

int tcgetattr(int fd, struct termios *termios_p);
功能:获取终端属性
参数:fd:打开串口设备节点描述符
termios_p:终端属性结构体指针

int tcsetattr(int fd, int optional_actions,
const struct termios *termios_p);
功能:设置终端属性
参数:fd:打开串口设备节点描述符
termios_p:终端属性结构体指针:有70多种标志(这里不详细介绍,后面会说)
optional_actions:TCSANOW: 更改立即发生
TCSADRAIIN: 发送所有输出后更改才发生,更改输出参数选用这个
TCSAFLUSH: 发送所有输出后更改才发生,更改时所有未读数据全部丢弃
  1. 波特率
#include
#include

speed_t cfgetispeed(const struct termios *termios_p);
speed_t cfgetospeed(const struct termios *termios_p);
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
功能:获取/设置波特率
参数:termios_p:struct termios结构体指针
speed:波特率:B50、B75、B110、B150、B200、B300、B600、B1200、B1800、B2400、B4800、B9600、B19200、B38400、B57600、B115200
返回值:成功0 失败-1.

在调用cfget函数之前先调用tcgetattr函数获取struct termios结构指针
  1. 控制函数
int tcflush(int fd, int queue_selector);
功能:冲洗缓冲区
参数:fd:打开串口设备节点描述符
queue_selector:TCIFLUSH:冲洗输入队列
TCOFLUSH:冲洗输出队列
TCIOFLUSH:冲洗输入和输出缓冲队列
返回值:成功0 失败-1

int tcsendbreak(int fd, int duration);
功能:指定时间区间内发送连续的0值位流
参数:fd:打开串口设备节点描述符
duration: 0:传递延续0.25-0.5s
非0:传递时间依赖于实现
返回值:成功0 失败-1

int tcdrain(int fd);
功能:等待所以输出都被传递
返回值:成功0 失败-1

int tcflow(int fd, int action);
功能:对输入输出流进行控制
参数:action:TCOOFF:输出被挂起
TCOON:启动被挂起的输出
TCIOOFF:发送一个stop,终端设备停止发送数据
TCION: 发送一个START,终端设备继续发送数据
返回值:成功0 失败-1
  1. Linux系统下串口编程实现demo
#include
#include
#include
#include

#include
#include
#include
#include
#include

#define ARRAY_SIZE(A) (sizeof(A)/sizeof(A[0]))
#define MAX_BAUD 115200
#define UART_IDX "/dev/ttyUSB0"
#define CRC_OK 0
#define CRC_FAIL -1

typedef enum {
STANDARD_INPUT_MODE = 1,
RAWDATA_MODE
} uart_mode_e;

static int fd;
static pthread_t read_thread_id;
static int usb_thread_run;
static char recv_buf[1024 * 100];
static unsigned char recvmsg[1024 * 100];



static int ws_uart_send(char *buf, int len)
{
unsigned int total_byte = 0;
int send_byte;
while (len > 0) {
if (len < 1024)
send_byte = write(fd, buf+total_byte, len);
else
send_byte = write(fd, buf+total_byte, 1024);
if (send_byte < 0) {
tcflush(fd, TCOFLUSH);
printf("data send errorn");
return -1;
}
len -= send_byte;
total_byte += send_byte;
printf("len = %d total_byte = %dn", len, total_byte);
}
return 0;
}


static void *read_thread(void *arg)
{
int count,ret = 0;
int cnt;
int total;
char buf[64] = {0};
usb_thread_run = 1;
while (usb_thread_run) {
memset(recv_buf, 0, sizeof(recv_buf));
cnt = 0;
total = 0;
count = read(fd, buf, 64);

printf("cnt = %dn", count);
printf("buf = %sn", buf);
write(fd, buf, count);

}
return NULL;
}
/**
* @brief
* @note
* @param fd:
* @param baud_rate:
* @retval
*/
int set_uart_baud_rate(int fd, int baud_rate)
{
int ret = 0;
struct termios param;
int speed_arr[] = { B921600, B576000, B500000, B460800, B230400,B115200, B38400, B19200, B9600, B4800, B2400, B1200 };
int name_arr[] = { 921600, 576000, 500000, 460800, 230400,115200, 38400, 19200, 9600, 4800, 2400, 1200 };
int i = 0;
int status;

status = tcgetattr(fd, ¶m);
if(0 != status)
{
printf("%s:%d error = %dn",__func__, __LINE__, ret);
return ret;
}
for(i = 0; i < ARRAY_SIZE(speed_arr); i++)
{
if(baud_rate == name_arr[i])
{
tcflush(fd, TCIOFLUSH);
if(0 != cfsetispeed(¶m, speed_arr[i]))
{
printf("%s:%d error = %dn",__func__, __LINE__, ret);
return ret;
}
if(0 != cfsetospeed(¶m, speed_arr[i]))
{
printf("%s:%d error = %dn",__func__, __LINE__, ret);
return ret;
}
status = tcsetattr(fd, TCSANOW, ¶m);
if(0 != status)
{
printf("%s:%d error = %dn",__func__, __LINE__, ret);
return ret;
}
tcflush(fd, TCIOFLUSH);
break;
}
}
if(i == ARRAY_SIZE(speed_arr))
{
printf("%s:%d error = %dn",__func__, __LINE__, ret);
return ret;
}
return ret;
}


/**
* @brief
* @note
* @param fd:
* @param databits:
* @param stopbits:
* @param parity:
* @param mode:
* @retval
*/
int set_uart_parity(int fd, int databits, int stopbits, int parity, uart_mode_e mode)
{
int ret = 0;
struct termios param;
if(tcgetattr(fd, ¶m) != 0)
{
printf("%s:%d error = %dn",__func__, __LINE__, ret);
return ret;
}
param.c_cflag &= ~CSIZE;
switch(databits) /*设置数据位数*/
{
case 7:
param.c_cflag |= CS7;
break;
case 8:
param.c_cflag |= CS8;
break;
default:
return ret;
}
switch(parity)
{
case 'n':
case 'N':
param.c_cflag &= ~PARENB; /* Clear parity enable */
param.c_iflag &= ~(INPCK | ICRNL | IXON); /* Enable parity checking */
break;
case 'o':
case 'O':
param.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/
param.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'e':
case 'E':
param.c_cflag |= PARENB; /* Enable parity */
param.c_cflag &= ~PARODD; /* 转换为偶效验*/
param.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'S':
case 's': /*as no parity*/
param.c_cflag &= ~PARENB;
param.c_cflag &= ~CSTOPB;
break;
default:
return ret;
}
/* 设置停止位*/
switch(stopbits)
{
case 1:
param.c_cflag &= ~CSTOPB;
break;
case 2:
param.c_cflag |= CSTOPB;
break;
default:
return ret;
}
if(mode == STANDARD_INPUT_MODE)
/*标准输入设置*/
{
param.c_lflag &= ~(ECHO); //关闭回显
param.c_lflag |= (ICANON);
param.c_oflag |= OPOST; //
}
/*raw data mode*/
else if(mode == RAWDATA_MODE)
{
param.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
param.c_oflag &= ~OPOST; //raw output
}
/* Set input parity option */
if(parity != 'n')
{
param.c_iflag |= INPCK;
}

param.c_cc[VTIME] = 10; //10 // 1 seconds
param.c_cc[VMIN] = 0;

tcflush(fd, TCIFLUSH);

/* Update the options and do it NOW */
if(tcsetattr(fd, TCSANOW, ¶m) != 0)
{
printf("%s:%d error = %dn",__func__, __LINE__, ret);
return ret;
}
return ret;
}

void uart_recv_start(void)
{
printf("recv start");
pthread_create(&read_thread_id, NULL, read_thread, NULL);
pthread_detach(read_thread_id);
}

void uart_deinit()
{
usb_thread_run = 0;
close(fd);
pthread_join(read_thread_id, NULL);
}

int main(int argc, char* argv[])
{
struct termios oldtio, newtio;
int ret = 0;
char buf[256];
fd = open(argv[1], O_RDWR | O_NOCTTY);
if (fd < 0) {
printf("Open %s failedn", argv[1]);
return -1;
} else
printf("Open %s successfullyn", argv[1]);

set_uart_baud_rate(fd, 115200);
ret = set_uart_parity(fd, 8, 1, 'n', RAWDATA_MODE);
uart_recv_start();
while (1) {
sleep(1);
}

}
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分