文章目录
- 1 基于文件指针的文件操作
- 1.1 文件的创建,打开和关闭
- 1.2 文件读写操作
- 2 基于文件描述符的文件操作
- 2.1 打开、创建和关闭文件
- 2.2 文件读写
- 2.3 改变文件大小
- 2.4 文件映射
- 2.5 文件定位
- 2.6 获取文件信息
- 2.7 复制文件描述符
- 2.8 文件描述符和文件指针
- 2.9 标准输入输出文件描述符
- 2.10 管道
- 3 Linux的目录操作
- 3.1 获取和切换当前目录
- 3.2 创建和删除目录
- 3.3 目录的存储原理
- 3.4 目录相关操作
- 4 I/O 多路转接模型
- 4.1 读取文件的阻塞
- 4.2 I/O多路转接模型和select
- 4.3 select的退出机制
- 4.4 select函数的超时处理机制
- 4.5 写集合的原理
- 写在最后,这篇文章作者写了好久,其中的内容完全是知识盲区,希望读者仔细阅读,持续关注,下一篇内容讲解Linux的进程。
使用环境:Ubuntu18.04
使用工具:VMWare workstations ,xshell
作者在学习Linux的过程中对常用的命令进行记录,通过思维导图的方式梳理知识点,并且通过xshell连接vmware中ubuntu虚拟机进行操作,并将练习的截图注解,每句话对应相应的命令,读者可以无障碍跟练。第四次练习的重点在于Linux的目录操作和文件操作。
1 基于文件指针的文件操作
Linux 中对目录和设备的操作都是文件操作,文件分为普通文件,目录文件,链接文件和设备文件。在Linux中对文件的操作,是使用文件指针来访问文件的方法是由标准 C 规定的,基于文件指针的文件操作函数是 ANSI 标准函数库的一部分。所以本次练习基本是复习C语言操纵文件的功能。其中mmap的内容先按下不表,讲完目录操作后继续。
1.1 文件的创建,打开和关闭
#include <stdio.h> //头文件包含
FILE* fopen(const char* path, const char* mode);//文件名 模式
int fclose(FILE* stream);
fopen 创建的文件的访问权限将以 0666 与当前的 umask 结合来确定。
下面案例中使用了wb方式创建了文件,并且关闭文件。
1.2 文件读写操作
- 数据块读写操作
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);
fread 从文件流 stream 中读取 nmemb 个元素,写到 ptr 指向的内存中,每个元素的大小为 size 个字节
fwrite 从 ptr 指向的内存中读取 nmemb 个元素,写到文件流 stream 中,每个元素 size 个字节
- 格式化读写操作
#include <stdio.h>
int printf(const char *format, ...);
//相当于 fprintf(stdout,format,…);
int scanf(const char *format, …);
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, …);
int sprintf(char *str, const char *format, ...);
//eg:sprintf(buf,”the string is;%s”,str);
int sscanf(char *str, const char *format, …);
f 开头和s 开头的区别:
fprintf 将格式化后的字符串写入到文件流 stream 中
sprintf 将格式化后的字符串写入到字符串 str 中
注意fopen的参数需要改成wb+,支持读和写,否则什么都读不出来,作者测了半小时才发现是这个问题…
- 单个字符读写操作
#include <stdio.h>
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
int getc(FILE *stream);//等同于 fgetc(FILE* stream)
int putc(int c, FILE *stream);//等同于 fputc(int c, FILE* stream)
int getchar(void);//等同于 fgetc(stdin);
int putchar(int c);//等同于 fputc(int c, stdout);
- 字符串读写操作
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
int puts(const char *s);//等同于 fputs(const char *s,stdout);
char *gets(char *s);//等同于 fgets(const char *s, int size, stdin);
- 文件定位操作:
rewind函数已经使用过了,其余的请读者自行练习
#include <stdio.h>
int feof(FILE * stream);
//通常的用法为 while(!feof(fp)),没什么太多用处
int fseek(FILE *stream, long offset, int whence);
//设置当前读写点到偏移 whence 长度为 offset 处
long ftell(FILE *stream);
//用来获得文件流当前的读写位置
void rewind(FILE *stream);
//把文件流的读写位置移至文件开头 fseek(fp, 0, SEEK_SET);
2 基于文件描述符的文件操作
POSIX标准支持另一类不带缓冲区的IO。使用文件描述符描述文件,文件描述符是一个非0整数。原理上来说,每次打开文件,进程地址空间内核部分会维护一个已经打开的文件的数组,文件描述符就是这个数组的索引。因此文件描述符可以实现进程和打开文件之间的交互。
2.1 打开、创建和关闭文件
使用open函数可以打开或者创建一个并打开一个文件,使用creat函数可以创建一个文件
#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);//文件名 打开方式 权限
int creat(const char *pathname, mode_t mode);//文件名 权限
//creat 现在已经不常用了,它等价于
open(pathname,O_CREAT|O_TRUNC|O_WRONLY,mode);
#include <unistd.h>
int close(int fd);//fd 表示文件描述词,是先前由 open 或 creat 创建文件时的返回值。
从上述man手册中可以看到,函数的返回值都是int类型,也就是说,函数执行成功后会返回一个文件描述符,表示已经打开的文件;执行失败会返回-1,并设置相应的errno。flags表示打开或创建的方式,mode表示文件的访问权限。
掩码 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开 |
O_WRONLY | 以只写的方式打开 |
O_RDWR | 以读写的方式打开 |
O_CREAT | 如果文件不存在,则创建文件 |
O_EXCL | 仅与 O_CREAT 连用,如果文件已存在,则 open 失 |
O_APPEND | 已追加的方式打开文件,每次调用 write 时,文件指针自动先移到文件尾,用于多进程写同一个文件的情况。 |
O_NONBLOCK | 非阻塞方式打开,无论有无数据读取或等待都会立即返回进程之中 |
O_NODELAY | 非阻塞方式打开 |
O_SYNC | 同步打开文件,只有在数据被真正写入物理设备设备后才返回 |
文件使用后,要记得使用close关闭文件。close关闭后,该进程队文件所加的锁全部被释放,并且是文件的打开索引计数-1,只有文件的打开引用计数变为0后,文件才会被真正的关闭。使用ulimit -a 命令可以查看单个进程能同事打开文件的上限。下面展示open函数创建文件的操作,其他函数和参数读者自行尝试。
2.2 文件读写
使用read函数和write函数,他们统称为不带有缓冲区的IO。读取完了返回0,出错返回-1,其余情况返回读写的字符数。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);//文件描述符 缓冲区 长度
ssize_t write(int fd, const void *buf, size_t count);
注意: 作者在测试读写案例时发现file1文件忘记加权限导致,这个案例花费了好久,读者可以自行修改上面的main.c源文件,添加权限参数,也可以使用如下命令修改文件权限。
修正案例如下:
2.3 改变文件大小
ftruncate 函数可以改变文件大小,必须以写入模式 打开文件,如果文件大小比参数length大,就会删除超过的部分(实际上是修改了文件的inode信息)。成功返回0,否则返回-1;
#include <unistd.h>
int ftruncate(int fd, off_t length);
2.4 文件映射
- 使用 mmap 接口可以实现直接将一个磁盘文件映射到存储空间的一个缓冲区上
面,无需使用 read 和 write 进行 IO
#include <sys/mman.h>
void *mmap(void *adr, size_t len, int prot, int flag, int fd, off_t offset);
adr参数用于指定映射存储区的起始地址。设为NULL,由操作系统自动分配(通常在堆空间)。fd参数是一个文件描述符,必须是打开的状态。prot参数表示权限,PROT_READ,PROT_WRITE 表示可读可写,flag表示这片空间是否可以反映到磁盘上的参数,MAP_SHARED、MAP_PRIVATE。offset参数需是 4k 的整数倍。
- 使用 mmap 函数经常配合函数 ftruncate 来扩大文件大小,原因是分配的缓冲区大小和偏移量大小是有限制的,必须是虚拟内存页大小的整数倍。如果文件较小,那么超过文件大小返回的缓冲区操作将不会修改文件。如果文件大小为0,还会出现Bus error异常。
2.5 文件定位
- 函数lseek 将文件指针设定到相对于whence,偏移值为offset的位置。他的返回值是读写点距离文件开始的距离。前面使用过了,这里不再做演示了。
- 利用lseek函数可以实现文件空洞,即一个空文件,可以定位到便宜文件开始1024字节的地方,再写入一个字符,相当于给该文件分配了1025个字节,形成文件空洞。通常用于多进程之间通信的共享内存。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);//fd 文件描述词
//whence 可以是下面三个常量的一个
//SEEK_SET 从文件头开始计算
//SEEK_CUR 从当前指针开始计算
//SEEK_END 从文件尾开始计算
2.6 获取文件信息
- 通过fstat和stat函数获取文件信息,调用后文件信息被填充到结构体struct stat变量中。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *file_name, struct stat *buf); //文件名 stat 结构体指针
int fstat(int fd, struct stat *buf); //文件描述词 stat 结构体指针
struct stat
{
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
};
同时对于struct stat结构体st_node,有一组宏可以进行文件类型的判断:
宏 | 描述 |
---|---|
S_ISLNK(mode) | 判断是否是符号链接 |
S_ISREG(mode) | 判断是否是普通文件 |
S_ISDIR(mode) | 判断是否是目录 |
S_ISCHR(mode) | 判断是否是字符型设备 |
S_ISBLK(mode) | 判断是否是块设备 |
S_ISFIFO(mode) | 判断是否是命名管道 |
S_ISSOCK(mode) | 判断是否是套接字 |
2.7 复制文件描述符
- 系统调用函数dup函数,参数是一个旧的文件描述符,返回一个新的文件描述符,这个新的文件描述符是旧文件描述符的拷贝。
- 系统调用函数dup2函数,参数是一个旧的文件描述符和一个新的文件描述符,函数成功后,新的文件描述符编程旧的文件描述符的拷贝。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
- 正常情况下,如果直接用整型变量拷贝文件描述符,使得两个变量都指向一个打开的文件,但是内核中文件打开的引用计数还是1,无论是close哪一个都会导致文件的关闭。而如果使用dup或者dup2函数则不会出现这种情况。
int fd = open(argv[1],O_RDWR);
int fd1 = fd;
close(fd);//会导致文件关闭
char buf[128] = {0};
int ret = read(fd1, buf, sizeof(buf)); //读取失败
- dup的原理,当使用文件时,进程地址空间应当分配一篇空间存放打开文件的inode信息,此时文件已经调入内存,Linux使用链表的方式管理inode信息,即inode表。inode表中如果该文件的引用计数为0,则从inode表中删除该文件的inode表项。dup操作正是拷贝了inode表项,使得inode的引用计数+1,所以close其中一个拷贝时,不会导致文件关闭。
- 使用dup函数重定向,序首先打开了一个文件,返回一个文件描述符,因为默认的就打开了 0,1,2 表示标准输入,标准输出,标准错误输出。用 close(STDOUT_FILENO);则表示关闭标准输出,此时文件描述符 1 就空着然后dup(fd);则会复制一个文件描述符到当前未打开的最小描述符,此时这个描述符为 1。后面关闭 fd 自身,然后在用标准输出的时候,发现标准输出重定向到你指定的文件了。那么 printf所输出的内容也就直接输出到文件(因为 printf 的原理就是将内容输入到描述符为 1 的文件里面)。
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
int fd = open(argv[1],O_RDWR);
ERROR_CHECK(fd,-1,"open");
printf("\n");
close(STDOUT_FILENO);
int fd1 = dup(fd);
printf("fd1 = %d\n", fd1);
close(fd);
printf("the out of stdout\n");
return 0;
}
2.8 文件描述符和文件指针
- fopen 函数实际在运行的过程中也获取了文件的文件描述符。使用 fileno 函数可以得到文件指针的文件描述符。当使用 fopen 获取文件指针以后,依然是可以使用文件描述符来执行 IO。
printf("fd = %d\n", fd);
char buf[128] = {0};
read(fd, buf, 5);
printf("buf = %s\n", buf);
//使用 read 接口也是能够正常读取内容的
- fopen原理,fopen执行时会先调用open函数,打开文件并且获取文件的信息,然后fopen函数会在用户态申请一块空间作为缓冲区。
- fopen的好处,因为read和write是系统调用函数,需要频繁在用户态和核心态切换,耗时间多。而借助用户态缓冲区,可以先将文件内容读入缓冲区,后续再对文件进行操作。
- fdopen函数可以根据文件描述符fd 生成用户态缓冲区,mode包括r、w、a、r+、w+、a+几种类型。
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
- 注意如果获取了文件指针,不要使用文件描述符的方式关闭文件,如下操作:
FILE* fp = fopen(argv[1],"rb+");
close(fileno(fp));//如果使用 fd=fileno(fp),那么 close 以后 fd 的数值不会发生改变
//出现报错 fgets: Bad file descriptor
2.9 标准输入输出文件描述符
- 与标准的输入输出流对应,在更底层的实现是用标准输入、标准输出、标准错误文件描述符表示的。它们分别用 STDIN_FILENO、STDOUT_FILENO 和STDERR_FILENO 三个宏表示,值分别是 0、1、2 三个整型数字
2.10 管道
- 管道文件用于数据通信的一种文件,半双工通信,它在 ls -l 命令中显示为p,管道文件无法存储数据。
传输方式 | 含义 |
---|---|
全双工 | 双方可以同时向另一方发送数据 |
半双工 | 某个时刻只能有一方向另一方发送数据,其他时刻的传输方向可以相反 |
单工 | 永远只能由一方向另一方发送数据 |
- linux命令操作管道
$ mkfifo [管道名字]
使用 cat 打开管道可以打开管道的读端
$ cat [管道名字]
打开另一个终端,向管道当中输入内容可以实现写入内容
$ echo “string” > [管道名字]
此时读端也会显示内容
- 注意:禁止使用vim打开编译管道文件!!
3 Linux的目录操作
3.1 获取和切换当前目录
- getcwd函数将目前的工作目录绝对路径复制到buf所指的内存空间中,参数size为buf的空间大小。若buf为NULL,getcwd会根据size的大小自动配置内存,如果size也为0,getcwd会根据目录字符串大小来分配相应大小的空间,进程使用完字符串后会自动free释放空间。最常用的形式
getcwd(NULL, 0);
#include <unistd.h> //头文件
char *getcwd(char *buf, size_t size); //获取当前目录,相当于 pwd 命令
char *getwd(char *buf);
char *get_current_dir_name(void);
int chdir(const char *path); //修改当前目录,即切换目录,相当于 cd 命令
- chdir函数:用来修改当前工作目录,修改成参数path所指的目录,读者自行尝试。
#include<unistd.h>
int main()
{
chdir(“/tmp”);
printf(“current working directory: %s\n”,getcwd(NULL,0));
}
3.2 创建和删除目录
- 创建和删除目录的函数和Linux下创建和删除目录的命令一样
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int mkdir(const char *pathname, mode_t mode); //创建目录,mode 是目录权限
int rmdir(const char *pathname); //删除目录
- 如何修改环境变量
查看环境变量
$ echo $PATH
修改环境(系统路径)变量(只对本次生效)
$ export PATH=$PATH:新目录
3.3 目录的存储原理
- 为了定位文件在磁盘中的位置,文件系统使用专门的索引结构来管理所有的文件。索引结构的基本单位是索引结点,其中包含了文件的位置、文件类型、权限、修改时间等。文件系统将所有索引结点用数组存储起来,并利用一个位图实现高效管理文件信息。
- Linux中目录是一种特殊文件,目录的大小总是固定的。目录的数据块中吧很多文件的文件名和索引结点存放在一起。因为文件名大小不一,所以采取链式结构。链式结构的结点就是dirent结点,定义如下:
struct dirent{
ino_t d_ino; //该文件的 inode
off_t d_off; //到下一个 dirent 的偏移
unsigned short d_reclen;//文件名长度
unsigned char d_type; //所指的文件类型
char d_name[256]; //文件名
};
3.4 目录相关操作
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name); //打开一个目录
struct dirent *readdir(DIR *dir); //读取目录的一项信息,并返回该项信息的结构体指针
void rewinddir(DIR *dir); //重新定位到目录文件的头部
void seekdir(DIR *dir,off_t offset);//用来设置目录流目前的读取位置
off_t telldir(DIR *dir); //返回目录流当前的读取位置
int closedir(DIR *dir); //关闭目录文件
- 读取目录信息的步骤:
- 用opendir函数打开目录,获得DIR指针。DIR称为目录流,类似于标准输入输出,每次使用readdir后,它会将位置移动到下一个文件。
- 使用readdir函数迭代读取目录的内容
- 用closedir函数关闭目录
- inode(索引结点)描述了文件在磁盘上的具体位置信息。在ls命令中添加 -i参数可以查看文件的inode信息。那么所谓的硬链接,就是指inode相同的文件。一个inode的节点上的硬链接个数就成为引用计数。软链接不计。
$ ls -ial
查看所有文件的 inode 信息
$ ln 当前文件 目标
建立名为“目标”的硬链接
- 当inode计数为0时,才会将磁盘内容移出文件管理系统,即断开和目录的链接。为了避免引起死锁,普通用户不能使用ln命令为目录建立硬链接。
- 看一个深度优先遍历访问目录的例子:
- seekdir()函数用来设置目录流目前的读取位置,再调用 readdir()函数时,便可以从此新位置开始读取。参数 offset 代表距离目录文件开头的偏移量
- 使用 readddir()时,如果已经读取到目录末尾,又想重新开始读,则可以使用rewinddir 函数将文件指针重新定位到目录文件的起始位置
- telldir()函数用来返回目录流当前的读取位置
4 I/O 多路转接模型
4.1 读取文件的阻塞
- 阻塞:在目前的模式下,read函数如果不能从文件中读取内容,就将进程的状态切换到阻塞状态,不再继续执行
//在写端写入时添加 sleep(10)
...
sleep(10);
write
...
//再次测试的时候发现读端会明显延迟
- 实现即时聊天: 管道文件是半双工通信,可以使用2个管道文件实现全双工通信,即两个进程分别监听两个管道文件,一边读一边写。有读者就比较疑惑了,为什么用两个管道文件就能实现,两个普通文件无法实现通信呢?原因就在于管道文件是半双工通信,其中没有内容的时候可以阻塞进程。代码如下:
//1 号进程
#include "header.h"
#include <stdio.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,3);
int fdr = open(argv[1],O_RDONLY);//管道打开的时候,必须要先将读写端都打开之后才能继续
int fdw = open(argv[2],O_WRONLY);
printf("I am chat1\n");
char buf[128] = {0};
while(1)
{
memset(buf,0,sizeof(buf));//将buf缓冲区清0
read(STDIN_FILENO, buf, sizeof(buf));//将标准输入的字符串写入buf缓冲区
write(fdw, buf, strlen(buf)-1);//写入2号管道文件
memset(buf,0,sizeof(buf));
read(fdr, buf, sizeof(buf));//读取1号管道文件
printf("buf = %s\n", buf);
}
return 0;
}
//2 号
#include "header.h"
#include <stdio.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,3);
int fdw = open(argv[1],O_WRONLY);//管道打开的时候,必须要先将读写端都打开之后才能继续
int fdr = open(argv[2],O_RDONLY);
printf("I am chat2\n");
char buf[128] = {0};
while(1)
{
memset(buf,0,sizeof(buf));
read(fdr, buf, sizeof(buf));//读取1号管道文件
printf("buf = %s\n", buf);
memset(buf,0,sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));//从标准输入读取字符串到buf缓冲区
write(fdw, buf, strlen(buf)-1);//写入2号管道文件
}
return 0;
}
//这里经常会有阻塞
- 使用管道文件达成的实时聊天,是有很明显的缺陷,必须两个进程一人说一句话,不能连续说多句话,总一方总是在等待,后序将会继续完善通信的操作。
- 这里拓展一点内容,我在408考研中了解的进程通信。进程通信的方式有三种,第一种是共享内存,即通信的进程共享一块内存空间。第二种通信方式是消息传递,两个进程使用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发送消息和接受消息两个原语进行数据交换。第三种就是上面演示的管道通信,管道实际上就是一个缓冲区,其大小在linux中设定为4KB,管道的读操作比写操作要快,当管道文件中的数据被读取后系统会调用read()阻塞,等待数据的输入。
4.2 I/O多路转接模型和select
- I/O多路转接模型:如果请求的I/O操作阻塞,且它不是真正的阻塞I/O,而是让其中一个函数等待,在这期间,I/O 还能进行其他操作。接下来要介绍的 select()函数,就是属于这种模型。
- 使用select函数的原因,select可以完成非阻塞方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况—— 读写或是异常 。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set *readset,fd_set *writeset, fd_set *exceptionset, struct timeval
* timeout);
//返回值为0 代表超时,返回值为-1 代表出错
/*
select函数的参数解释:
maxfd:最大的文件描述符(其值应该为最大的文件描述符字 + 1)
readset:内核读操作的描述符字集合
writeset:内核写操作的描述符字集合
exceptionset:内核异常操作的描述符字集合
timeout:等待描述符就绪需要多少时间。NULL 代表永远等下去,一个固定值代表等待固定时间,0 代表根本不等待,检查描述字之后立即返回
*/
//readset、writeset、exceptionset 都是 fd_set 集合
//集合的相关操作如下:
void FD_ZERO(fd_set *fdset); /* 将所有 fd 清零 */
void FD_SET(int fd, fd_set *fdset); /* 增加一个 fd */
void FD_CLR(int fd, fd_set *fdset); /* 删除一个 fd */
int FD_ISSET(int fd, fd_set *fdset); /* 判断一个 fd 是否有设置 */
- 一般情况下,使用select之前,要使用 FD_ZERO 和 FD_SET 来初始化文件描述符集,在使用 select 函数时,可循环使用 FD_ISSET 测试描述符集,在执行完对相关文件描述符之后,使用 FD_CLR 来清除描述符集。
//chat1.c
//编译后运行
//$ ./chat1 1.pipe 2.pipe
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,3);
int fdr = open(argv[1],O_RDONLY);//管道打开的时候,必须要先将读写端都打开之后才能继续
int fdw = open(argv[2],O_WRONLY);//对管道2只读打开
printf("I am chat1\n");
char buf[128] = {0};//设置缓冲区
int ret;
fd_set rdset;//rdset文件描述符
while(1){
FD_ZERO(&rdset);//清空fdset中的fd
FD_SET(STDIN_FILENO,&rdset);//将标准输入添加到集合中
FD_SET(fdr,&rdset);//将只读的管道文件1添加到集合中
ret = select(fdr+1, &rdset, NULL, NULL, NULL);//设置最大文件操作符,将rdset设为读区的描述字集合
if(FD_ISSET(STDIN_FILENO, &rdset)){ //判断rdset集合中是否设置了标准输入输出
memset(buf,0,sizeof(buf)); //清空缓冲区
read(STDIN_FILENO, buf, sizeof(buf)); //读取标准输入的内容到buf缓冲区中
write(fdw, buf, strlen(buf)-1); //将buf缓冲区的内容写入管道文件2,即fdw打开的只写文件
}
if(FD_ISSET(fdr, &rdset)){ //判断rdset集合中是否设置了只读文件的描述字
memset(buf,0,sizeof(buf)); //清空缓冲区
read(fdr, buf, sizeof(buf)); //将fdr对应的只读文件内容读取到缓冲区buf
printf("buf = %s\n", buf); //打印buf内容
}
}
return 0;
}
//chat2.c
//编译后运行(注意管道建立连接的顺序)
//$ ./chat2 1.pipe 2.pipe
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,3);
int fdw = open(argv[1],O_WRONLY);//管道打开的时候,必须要先将读写端都打开之后才能继续
int fdr = open(argv[2],O_RDONLY);
printf("I am chat2\n");
char buf[128] = {0};
int ret;
fd_set rdset;
while(1){
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO,&rdset);
FD_SET(fdr,&rdset);
ret = select(fdr+1, &rdset, NULL, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rdset)){
memset(buf,0,sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
write(fdw, buf, strlen(buf)-1);
}
if(FD_ISSET(fdr, &rdset)){
memset(buf,0,sizeof(buf));
read(fdr, buf, sizeof(buf));
printf("buf = %s\n", buf);
}
}
return 0;
}
- 注意: 作者在练习这段代码的时候发现,chat2.c中的fdr和fdw定义语句不能交换,否则会进程会一直等待,必须使用Ctrl+Z终止。原因就是如果两个文件同时以只读的方式打开管道文件,都在等待一个写进程进入管道,就会导致互相等待。
- fdset 实际上是一个文件描述符的位图,采用数组的形式来存储。下面是一个简化版本的实现方法:
//fd_set 的成员是一个长整型的结构体
typedef long int __fd_mask;
//将字节转化为位
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
//位图-判断是否存在文件描述符 d
#define __FD_MASK(d) ((__fd_mask) (1UL << ((d) % __NFDBITS)))
//select 和 pselect 的 fd_set 结构体
typedef struct
{
//成员就是一个长整型的数组,用来实现位图
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;
// fd_set 里面文件描述符的数量,可以使用 ulimit -n 进行查看
#define FD_SETSIZE __FD_SETSIZE
- maxfd 是最大描述符加 1 的原因, 当传入 fdmax 的时候,select 会监听0~fdmax-1 的文件描述符。
4.3 select的退出机制
- 管道写端先关闭的时候,读端的read会返回一个0,操作系统会将管道状态设置为可读,这个可读状态会导致select函数不会阻塞,进入死循环。如果读端先关闭,写端会直接崩溃。(后续网络编程会讲)
//将写端的程序修改为如此
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
int fdw = open(argv[1],O_WRONLY);
ERROR_CHECK(fdw,-1,"open");
printf("fdw = %d\n",fdw);
close(fdw);//这里将写端直接关闭
sleep(10);//然后睡眠 10s
return 0;
}
- 为了避免死循环,需要对退出读端继续兼容处理,就是当read的返回值为0的时候,就退出程序
if(FD_ISSET(STDIN_FILENO, &rdset)){ //判断集合中是否有标准输入
memset(buf,0,sizeof(buf)); //清空缓冲区
read_ret = read(STDIN_FILENO, buf, sizeof(buf)); //将标准输入的内容读取到buf缓冲区中
if(read_ret == 0){ //判断read是否为0,如果为0就break退出循环
printf("chat is broken!\n");
break;
}
write(fdw, buf, strlen(buf)-1); //否则就将读入的内容写入缓冲区中
}
if(FD_ISSET(fdr, &rdset)){
memset(buf,0,sizeof(buf));
read_ret = read(fdr, buf, sizeof(buf));
if(read_ret == 0){
printf("chat is broken!\n");
break;
}
printf("buf = %s\n", buf);
}
...
#使用 ctrl+c 终止程序会导致程序的返回值不为 0
#可以改用 ctrl+d 来终止 stdin(相当于输入了 EOF)
#$?代表了上个执行程序的返回值
$echo $?
4.4 select函数的超时处理机制
- 使用 timeval 结构体可以设置超时时间。传入 select 函数中的 timeout 参数是一个 timeval 结构体指针,timeval 结构体的定义如下:
struct timeval
{
long tv_sec;//秒
long tv_usec;//微秒
};
//用法
...
struct timeval timeout; //定义一个timeval的变量
while(1){
bzero(&timeout, sizeof(timeout));
timeout.tv_sec = 3;//设置等待时间,如果是NULL则永远等待,如果是0则不等待
ret = select(fdr+1, &rdset, NULL, NULL, &timeout);//传入参数
if(ret > 0){
...
}
else printf("time out!\n");
}
- 使用的超时判断的时候要注意,每次调用 select 之前需要重新为 timeout 赋值,因为调用 select 会修改 timeout 里面的内容。
4.5 写集合的原理
- 写阻塞和写就绪,当管道的写端向管道中写入数据达到上限后(4KB),后序的写入操作就会导致进程进入一个阻塞态,等待进程将数据从管道中读出,称为写阻塞。如果管道中的数据被读出,写端可以继续写入管道,就称为写就绪。
- select 也可以设置专门的写文件描述符集合,select 可以监听处于写阻塞状态下的文件,一旦文件转为写就绪,就可以将进程转换为就绪态。
#include <func.h>
int main(int argc, char* argv[])
{
ARGS_CHECK(argc, 2);
//用同一个管道进行测试i
int fdr = open(argv[1],O_RDWR);
int fdw = open(argv[1],O_RDWR);//可以一次性打开管道的读写端
fd_set rdset,wrset;
int ret;
char buf[128];
while(1){
FD_ZERO(&rdset); //清空写集合
FD_ZERO(&wrset); //清空读集合
FD_SET(fdr, &rdset);//将读操作放入读集合
FD_SET(fdw, &wrset);//将写操作放入写集合
ret = select(fdw+1, &rdset, &wrset, NULL, NULL);//设置select监听的读写集合位置
if(FD_ISSET(fdr, &rdset)){ //如果返回值不为0,即没有超时可以读
bzero(buf, sizeof(buf));//清空缓冲区buf
read(fdr, buf, sizeof(buf));//读取管道内容到缓冲区中
puts(buf);
usleep(250000); //进程挂起0.25秒
}
if(FD_ISSET(fdw, &wrset)){ //如果返回值不为0,即可以写
write(fdw,"helloworld", 10);
usleep(500000);//写后进程挂起0.5秒,给读操作的时间
}
}
}