Linux系统下I/O操作讲解,深入了解实战高级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. 文件描述符
对于内核而言,每个打开的文件都是通过文件描述符引用的,每个文件描述符都是一个非负整数,打开或者创建一个文件都会返回一个文件描述符,通过这个文件描述符来进行读写,
2. 打开/创建文件

	#include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    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

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

	#include <unistd.h>

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

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

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

    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.空洞文件在磁盘中不占用存储区。

5. 读文件

	#include <unistd.h>

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

6. 写文件

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

7. 文件共享
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. 存在多个文件描述符指向同一个文件的情况。

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

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

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

       	#include <unistd.h>
      
           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
      

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

     #include <unistd.h>
    
     void sync(void);
     	将修改过的块缓冲区排队写到队列就返回,数据并不一定写入到磁盘。命令sync就是调用sync函数。update系统守护进程每30s调用一次该函数。
     
     int fsync(int fd);
     	只对一个文件描述符其作用,并且磁盘操作结束后才返回。
    
     int fdatasync(int fd);
     	等同于fsync,但是同时更新文件属性。
    

    8. 修改已打开的文件属性

     #include <unistd.h>
     #include <fcntl.h>
    
     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
     		成功:其他
    

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

     #include <sys/ioctl.h>
      
     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操作
1. 流
在前面说的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 <stdio.h>

    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 <stdio.h>

    int fflush(FILE *stream);

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

	#include <stdio.h>

    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

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

	#include <stdio.h>

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

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

读写一个字符

	#include <stdio.h>

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

    int fputc(int c, FILE *stream);
    int putc(int c, FILE *stream);
    int putchar(int c);
	功能:写入文件数据
	参数:c 写入的字符  stream 流
	返回值:成功 写入的字符,失败 EOF

6. 读写一行字符

    #include <stdio.h>

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

	#include <stdio.h>

    int fputs(const char *s, FILE *stream);
    int puts(const char *s);
	功能:输出以null结尾的字符串数据数据到指定文件中
	参数:s 指定要被读取数据的缓冲区
			输出 \n 但不能输出\0
	返回值:成功 读取的字符串,失败 EOF;

7. 二进制I/O读写

	#include <stdio.h>

    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:流
	返回值:成功:返回实际写对象的个数
	注释:这两个函数存在一个问题就是只能读写同一系统上的数据,如果是不同系统则会造成问题。因为在不同系统同一结构的同一成员偏移量可能不同。

8. 定位流

	#include <stdio.h>

    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指示的值位置。

9. 格式化I/O

	格式化输出
	
	#include <stdio.h>
	
	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 中,并给其后添加一个字符串结束符 \0;
			如果格式化后的字符串长度大于 size,超过 size 的部分会被截断,只将其中的 (size-1) 个字符复制到 str 中,并给其后添加一个字符串结束符 \0,返回值为欲写入的字符串长度。
			失败:负数
	格式字符:  %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	输出数据域宽,数据长度<m,左补空格;否则按实际输出
			.n	对实数,指定小数点后位数(四舍五入)
			-	输出数据在域内左对齐(缺省右对齐)
			
			+	指定在有符号数的正数前显示正号(+)
			0	输出数值时指定左面不使用的空位置自动填0
			#	在八进制和十六进制数前显示前导0,0x
			l	long类型输出 %ld
				double类型输出 %lf

	格式化输入:
	#include <stdio.h>

    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。

	格式字符:同格式化输出

10. 临时文件

	#include <stdio.h>

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

	#include <stdio.h>

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

11 内存流
标准I/O库都是是将文件中数据取出来缓冲在内存中,现在我们可以直接通过缓冲区与主存直接来回传递数据,不依赖文件。仍然使用FILE指针,这些流看起来像文件流,其实是内存流。
内存流不访问文件只访问主存,所以如果标准I/O流作为参数用于临时文件的话,用内存流替代会有很大性能提高。

	#include <stdio.h>

    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 <wchar.h>

    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标志打开
2. 记录锁
记录锁:当一个进程正在读或者写一个文件某部分的时候,使用记录锁可以阻止其他进程修改同一文件区。

	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产生的子进程不继承父进程设置的锁。

3. I/O多路转接

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

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

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

  1. IO多路复用基本思想

     	先构造一张有关描述符的表,然后调用一个函数,当这些文件描述符中的一个或多个已准备好进行IO时函数才返回,函数返回时告诉进程已经有描述符就绪,可以进行IO操作。
    
  2. 实现函数select
    select函数可以使我们执行I/O多路转接,通过传给select函数的参数可以告诉内核:
    a.我们所关心的描述符
    b.对于每个描述符我们所关心的条件,是否想从一个给定描述符读/写,是否关心描述符异常
    c.愿意等待多长时间
    也可以通过返回值得到以下信息
    a.已经准备好的文件描述符
    b. 对于读、写、异常者三个条件中每一个,哪些已经准备好
    然后我们就可以使用read和write函数读写。

     		#include<sys/time.h>
     		#include<sys/types.h>
     		#include<unistd.h>
    
     		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.
    
  3. 实现函数poll
    poll函数与select函数相似,不同的是,poll不是为每个条件(读、写、异常)构造一个文件描述符,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号,poll函数可以用于任何类型的文件描述符。

     	#include <poll.h>
    
     	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.
    
  4. 散布读和聚集写
    就是在一次函数调用中读、写多个非连续的缓冲区。

     #include <sys/uio.h>
    
     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
    
    1. 存储映射I/O
      存储映射I/O,将一个磁盘文件映射到内存中的一个缓冲区上,从这个缓冲区读写数据就相当于读写文件数据,就可以不再使用read、write。

       #include <sys/mman.h>
       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 <termios.h>
    #include <unistd.h>

    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: 发送所有输出后更改才发生,更改时所有未读数据全部丢弃

2. 波特率

	#include <termios.h>
    #include <unistd.h>	
	
	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结构指针

3. 控制函数

	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

4. Linux系统下串口编程实现demo

	#include <stdio.h>
	#include <pthread.h>
	#include <unistd.h>
	#include <stdlib.h>

	#include <sys/types.h>
	#include <sys/stat.h>
	#include <fcntl.h>
	#include <termios.h>
	#include <string.h>

	#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 error\n");
				return -1;
			}
			len -= send_byte;
			total_byte += send_byte;
			printf("len = %d total_byte = %d\n", 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 = %d\n", count);
			printf("buf = %s\n", 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, &param);
		if(0 != status)
		{
			printf("%s:%d error = %d\n",__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(&param, speed_arr[i]))
				{
					printf("%s:%d error = %d\n",__func__, __LINE__, ret);
					return ret;
				}
				if(0 != cfsetospeed(&param, speed_arr[i]))
				{
					printf("%s:%d error = %d\n",__func__, __LINE__, ret);
					return ret;
				}
				status = tcsetattr(fd, TCSANOW, &param);
				if(0 != status)
				{
					printf("%s:%d error = %d\n",__func__, __LINE__, ret);
					return ret;
				}
				tcflush(fd, TCIOFLUSH);
				break;
			}
		}
		if(i == ARRAY_SIZE(speed_arr))
		{
			printf("%s:%d error = %d\n",__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, &param) != 0)
		{
			printf("%s:%d error = %d\n",__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, &param) != 0)
		{
			printf("%s:%d error = %d\n",__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 failed\n", argv[1]);
			return -1;
		} else
			printf("Open %s successfully\n", 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);
		}
		
	}
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值