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

news2024/11/18 9:31:36

文章目录

  • 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 文件读写操作

  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的进程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/388946.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Git标签与版本发布

1. 什么是git标签 标签&#xff0c;就类似我们阅读时的书签&#xff0c;可以很轻易找到自己阅读到了哪里。 对于git来说&#xff0c;在使用git对项目进行版本管理的时候&#xff0c;当我们的项目开发到一定的阶段&#xff0c;需要发布一个版本。这时&#xff0c;我们就可以对…

Spring 实战 第六版 学习笔记

Spring 实战 第六版 学习笔记 There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should leave quickly.…

【C语言督学训练营 第一天】课程导学,编程环境的安装

文章目录前言一、C语言学习常遇到的问题二、程序员职业发展三、C语言简介及环境搭建1.C语言简介2.安装编译器3.windows按装CLion前言 本系列是本人在复习408的时候记录的一些学习笔记&#xff0c;看的课程是王道的课程&#xff0c;只不过加入了一些个人的见解。不得不说王道的…

PTL仓库提货照明解决方案

仓库拣货到光解决方案是一种先进的导光技术&#xff0c;用货架空间上的全套电子显示装置&#xff08;如信号灯、显示器、确认按钮&#xff09;代替拣货单&#xff0c;简化仓储或配送&#xff1b;订单仓库中心履行流程的无纸化、高效且经济高效的解决方案。 什么是按光拣货系统(…

Java——包装类和List及ArrayList

目录 包装类&#xff08;Wrapped Class) 包装类的使用---装箱和拆箱 自动装箱和自动拆箱 Integer的易错题 javap反编译工具 List接口的使用 方法 ArrayList 使用 打印 区别 扩容机制 ArrayList练习 字符集合 杨辉三角 ​编辑 包装类&#xff08;Wrapped Class) Object 引用可…

CSS居中之 { left:50%; top:50%; transform:translate(-50%,-50%); }

CSS居中之 { left:50%; top:50%; transform:translate(-50%,-50%); } left:50%; top:50%; transform:translate(-50%,-50%); left:50%; top:50%; transform:translate(-50%,-50%);也可以写成: left:50%; top:50%; translate: -50% -50%; left:50%; top:50%; translate: -50%…

电子技术——CMOS反相器的动态响应

电子技术——CMOS反相器的动态响应 数字系统的速度&#xff08;例如计算机&#xff09;取决于其构成逻辑门的信号传播速度。因为反相器是数字逻辑门电路的基础&#xff0c;反相器的传播速度是一个很重要的特性。 传播延迟 传播延迟定义为反相器响应他的输入所需要的时间。特…

项目管理报告工具的功能

项目报告软件哪个好&#xff1f;Zoho Projects的项目管理报告工具为您提供整个组织的360可见性&#xff0c;获取所有项目的实时更新&#xff0c;使用强大的项目报告软件推动成功。Zoho Projects的项目报告软件允许团队整理和监控他们的资源和项目&#xff0c;以评估进度并避免对…

例1.10 几何概型题型一——(会面问题)

【例 1.10】&#xff08;会面问题&#xff09;甲乙两人约定在下午6 点到7点之间在某处会面,并约定先到者应等候另一人20 分钟,过时即可离去,求两人能会面的概率。我的答案&#xff1a;一、信息(1)对于甲乙会面约定事件是6~7点。(2)对于规则要求先到者等另一个人20分钟。(3)求两…

SAP会计科目打删除标记及如何物理删除

如果一个科目如果创建错误了&#xff0c;需要删除。如果在FS00上操作&#xff0c;点删除按钮&#xff0c;那么只是打删除标记而已&#xff08;相当于冻结&#xff09;。 删除和打删除标记是不一样的&#xff1a;打删除标记只是锁定该科目不再被用于记账业务&#xff0c;该科目仍…

进程概念~

进程概念 &#xff08;冯诺依曼体系结构&#xff0c;操作系统&#xff0c;进程概念&#xff0c;进程状态&#xff0c;环境变量&#xff0c;程序地址空间&#xff09; 冯诺依曼体系结构&#xff1a;&#xff08;计算机硬件体系结构&#xff09; 输入设备&#xff0c;输出设备&a…

【Java|基础篇】超详细讲解运算符

文章目录1. 什么是运算符2. 算术运算符隐式类型转换强制类型转换字符串的拼接字符相加自增和自减运算符3.赋值运算符4. 关系运算符5. 逻辑运算符短路与(&&)和短路或(||)6.三目运算符7. 位运算符8. 移位运算1. 什么是运算符 运算符用于执行程序代码运算&#xff0c;会针…

OpenCV-PyQT项目实战(11)项目案例07:摄像头操作与拍摄视频

欢迎关注『OpenCV-PyQT项目实战 Youcans』系列&#xff0c;持续更新中 OpenCV-PyQT项目实战&#xff08;1&#xff09;安装与环境配置 OpenCV-PyQT项目实战&#xff08;2&#xff09;QtDesigner 和 PyUIC 快速入门 OpenCV-PyQT项目实战&#xff08;3&#xff09;信号与槽机制 …

【大数据实时数据同步】超级详细的生产环境OGG(GoldenGate)12.2实时异构同步Oracle数据部署方案(中)

系列文章目录 【大数据实时数据同步】超级详细的生产环境OGG(GoldenGate)12.2实时异构同步Oracle数据部署方案(上) 【大数据实时数据同步】超级详细的生产环境OGG(GoldenGate)12.2实时异构同步Oracle数据部署方案(中) 文章目录系列文章目录前言安装OGG12C软件一、Linux本地GUI…

配置本地 python GEE、geemap环境

1.安装anconda 百度搜索anconda清华镜像&#xff0c;从清华镜像中选择最新的anconda安装包&#xff0c;国内镜像网站下载速度较快&#xff0c;如果从国外官网下载速度相当慢&#xff0c;详细安装教程请参考&#xff1a; anconda安装教程https://blog.csdn.net/lwbCUMT/article…

这些Python计算机视觉工具,帮你coding事半功倍

作为开发人员喜爱的语言之一&#xff0c;Python以其丰富的社区可用工具和库而闻名。我们列出了开发人员可以用于计算机视觉10个流行流行的Python库或平台&#xff0c;以帮助开发人员自动化开发任务&#xff0c;其中包括检测和可视化。1 | fastaifastai是一个深度学习库&#xf…

HBase读取流程详解

读流程从头到尾可以分为如下4个步骤&#xff1a;Client-Server读取交互逻辑&#xff0c;Server端Scan框架体系&#xff0c;过滤淘汰不符合查询条件的HFile&#xff0c;从HFile中读取待查找Key。其中Client-Server交互逻辑主要介绍HBase客户端在整个scan请求的过程中是如何与服务…

重构·改善既有代码的设计.01

前言近期在看Martin Fowler著作的《重构.改善既有代码的设计》这本书&#xff0c;这是一本经典著作。书本封面誉为软件开发的不朽经典。书中从一个简单的案例揭示了重构的过程以及最佳实践。同时给出了重构原则&#xff0c;何时重构&#xff0c;以及重构的手法。用来改善既有代…

Vue2.0开发之——购物车案例-Goods组件封装-商品名称和图片(46)

一 概述 循环渲染Goods组件为Goods组件封装title属性为Goods组件封装pic属性 二 循环渲染Goods组件 2.1 App.vue中导入Goods组件 import Goods from /components/Goods/Goods.vue2.2 App.vue中注册Goods组件 components: {Header,Goods}2.3 循环渲染每一个商品的信息 <…

记录--在Vue3这样子写页面更快更高效

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前言 在开发管理后台过程中&#xff0c;一定会遇到不少了增删改查页面&#xff0c;而这些页面的逻辑大多都是相同的&#xff0c;如获取列表数据&#xff0c;分页&#xff0c;筛选功能这些基本功能。而…