Linux 练习四 (目录操作函数 + 文件操作函数)

本文详细介绍了Linux系统中的文件操作,包括基于文件指针和文件描述符的两种方式,涵盖了文件的创建、打开、关闭、读写以及定位等基本操作。此外,还讨论了目录操作,如切换、创建和删除目录,以及目录的存储原理。文章还深入探讨了I/O多路转接模型,特别是select函数在非阻塞I/O中的应用,以及进程间通信的管道操作。最后,提到了进程通信的概念,包括共享内存和消息传递。
摘要由CSDN通过智能技术生成


使用环境: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 文件读写操作

  1. 数据块读写操作
#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 个字节
在这里插入图片描述

  1. 格式化读写操作
#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+,支持读和写,否则什么都读不出来,作者测了半小时才发现是这个问题…

在这里插入图片描述

  1. 单个字符读写操作
#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);

在这里插入图片描述

  1. 字符串读写操作
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);

在这里插入图片描述

  1. 文件定位操作
    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); //关闭目录文件
  • 读取目录信息的步骤:
  1. 用opendir函数打开目录,获得DIR指针。DIR称为目录流,类似于标准输入输出,每次使用readdir后,它会将位置移动到下一个文件。
  2. 使用readdir函数迭代读取目录的内容
  3. 用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秒,给读操作的时间
		} 
	} 
}

写在最后,这篇文章作者写了好久,其中的内容完全是知识盲区,希望读者仔细阅读,持续关注,下一篇内容讲解Linux的进程。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值