Linux 练习四 (目录操作 + 文件操作)
文章目录
- 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; //该文件的 inodeoff_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中的fdFD_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对应的只读文件内容读取到缓冲区bufprintf("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);//然后睡眠 10sreturn 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); //用同一个管道进行测试iint 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));//清空缓冲区bufread(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的进程。
相关文章:
Linux 练习四 (目录操作 + 文件操作)
文章目录1 基于文件指针的文件操作1.1 文件的创建,打开和关闭1.2 文件读写操作2 基于文件描述符的文件操作2.1 打开、创建和关闭文件2.2 文件读写2.3 改变文件大小2.4 文件映射2.5 文件定位2.6 获取文件信息2.7 复制文件描述符2.8 文件描述符和文件指针2.9 标准输入…...
自学大数据第四天~hadoop集群的搭建
Hadoop集群安装配置 当hadoop采用分布式模式部署和运行时,存储采用分布式文件系统HDFS,此时HDFS名称节点和数据节点位于不同的机器上; 数据就可以分布到多个节点,不同的数据节点上的数据计算可以并行执行了,这时候MR才能发挥其本该有的作用; 没那么多机器怎么办~~~~多几个虚拟…...
ULID和UUID
ULID:Universally Unique Lexicographically Sortable Identifier(通用唯一词典分类标识符)UUID:Universally Unique Identifier(通用唯一标识符)为什么不选择UUIDUUID 目前有 5 个版本:版本1&a…...
java基础面试10题
1.JVM、JRE 和 JDK 的关系 Jvm:java虚拟机,类似于一个小型的计算机,它能够将java程序编译后的.class 文件解释给相应平台的本地系统执行,从而实现跨平台。 jre:是运行java程序所需要的环境的集合,它包含了…...
Golang闭包问题及并发闭包问题
目录Golang闭包问题及并发闭包问题匿名函数闭包闭包可以不传入外部参数,仍然可以访问外部变量闭包提供数据隔离并发闭包为什么解决方法Golang闭包问题及并发闭包问题 参考原文链接:https://blog.csdn.net/qq_35976351/article/details/81986496 htt…...
基频的后处理
基频归一化 基频为什么要归一化?为了消除人际随机差异,提取恒定参数,在语际变异中找到共性。 引言 声调的主要载体就是基频。但是对声调的感知会因人而异,例如某个听感上的高升调,不同的调查人员可能会分别描写成 […...
vue3 toRefs详解
简介 toRefs函数的作用是将响应式对象中的所有属性转换为单独的响应式数据,对象成为普通对象,并且值是关联的。在这个过程中toRefs会做以下两件事: 把一个响应式对象转换成普通对象对该普通对象的每个属性都做一次ref操作,这样每…...
Spring——AOP是什么?如何使用?
一、什么是AOP?在不修改源代码的情况下 增加功能二、底层是什么?动态代理aop是IOC的一个扩展功能,现有IOC,再有AOP,只是在IOC的整个流程中新增的一个扩展点而已:BeanPostProcessorbean的创建过程中有一个步…...
【微服务】认识微服务
目录 1.1 单体、分布式、集群 单体 分布式 集群 1.2 系统架构演变 1.2.1 单体应⽤架构 1.2.2 垂直应⽤架构 1.2.3 分布式架构 1.2.4 SOA架构 1.2.5 微服务架构 1.3 微服务架构介绍 微服务架构的常⻅问题 1.4 SpringCloud介绍 1.4.1 SpringBoot和SpringCloud有啥关…...
【独家】华为OD机试 C 语言解题 - 最长连续子串
最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 最近更新的博客使用说明本期…...
【Linux】CentOS7操作系统安装nginx实战(多种方法,超详细)
文章目录前言一. 实验环境二. 使用yum安装nginx2.1 添加yum源2.1.1 使用官网提供的源地址(方法一)2.1.2 使用epel的方式进行安装(方法二)2.2 开始安装nginx2.3 启动并进行测试2.4 其他的一些用法:三. 编译方式安装ngin…...
【FMCW 01】中频IF信号
FMCW信号 调频连续波(frequency modulated continuous wave,FMCW)顾名思义,就是对信号的频率进行线性调制的信号。 从时域上看,对频率的调制,就像一把连续的锯齿波。其中每一个锯齿叫做一个chirp,其持续的时间叫做ch…...
【蓝桥杯试题】暴力枚举题型
💃🏼 本人简介:男 👶🏼 年龄:18 🤞 作者:那就叫我亮亮叭 📕 专栏:蓝桥杯试题 文章目录1. 统计方形(数据加强版)1. 1 题目描述1.2 思路…...
I.MX6ULL_Linux_系统篇(22) kernel移植
原厂 Linux 内核编译 NXP 提供的 Linux 源码肯定是可以在自己的 I.MX6ULL EVK 开发板上运行下去的,所以我们肯定是以 I.MX6ULL EVK 开发板为参考,然后将 Linux 内核移植到 I.MX6U-ALPHA 开发板上的。 配置编译 Linux 内核 和uboot一样,在编…...
UE实现相机聚焦物体功能
文章目录 1.实现目标2.实现过程2.1 实现原理2.2 源码浅析2.3 具体代码2.3.1 蓝图实现2.3.2 C++实现3.参考资料1.实现目标 实现根据输入的Actor,自动计算出其缩放显示到当前屏幕上相机的最终位置,然后相机飞行过去,实现相机对物体的聚集效果,避免每次输入FlyTo坐标参数,GI…...
算法系列之数值积分的目的
PLC算法里的数字积分器详细介绍请参看下面的文章链接: PLC算法系列之数值积分器(Integrator)_RXXW_Dor的博客-CSDN博客数值积分和微分在工程上的重要意义不用多说,闭环控制的PID控制器就是积分和微分信号的应用。流量累加也会用到。有关积分运算在流量累加上的应用,请参看下…...
【2.4 golang中循环语句for】
1. 循环语句for 1.1.1. Golang for支持三种循环方式,包括类似 while 的语法。 for循环是一个循环控制结构,可以执行指定次数的循环。 语法 Go语言的For循环有3中形式,只有其中的一种使用分号。 for init; condition; post { }for conditi…...
代码随想录 动态规划||343 96
Day35343. 整数拆分力扣题目链接给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。思路动规逻辑确定dp数组(dp table)以及下标的含义dp[i]指的是拆分数字i能得到的最大成绩d…...
Python---正则表达式
专栏:python 个人主页:HaiFan. 专栏简介:Python在学,希望能够得到各位的支持!!! 正则表达式前言概念作用和特点使用场景正则符号re模块re.compile()match()search()span()findall()group()sub()…...
Unity入门精要02---纹理
纹理和材质不可分割 本节知识结构 实践:简单贴一张纹理到模型上 首先在属性处添加相关属性 Properties {_Color ("Color Tint", Color) (1, 1, 1, 1)_MainTex ("Main Tex", 2D) "white" {}//加入纹理_Specular ("Specular&q…...
(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...
selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?
在工业自动化持续演进的今天,通信网络的角色正变得愈发关键。 2025年6月6日,为期三天的华南国际工业博览会在深圳国际会展中心(宝安)圆满落幕。作为国内工业通信领域的技术型企业,光路科技(Fiberroad&…...
零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...
