文件描述符的分配规则
看下面代码
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("myfile", O_RDONLY); if(fd < 0){ perror("open"); return 1; } printf("fd: %d\n", fd); close(fd); return 0; }
输出发现是 fd: 3
关闭0
或者2
,再看
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(0); //close(2); int fd = open("myfile", O_RDONLY); if(fd < 0){ perror("open"); return 1; } printf("fd: %d\n", fd); close(fd); return 0; }
发现是结果是: fd: 0
或者 fd: 2
可见,文件描述符的分配规则:在files_struct
数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符
C标准库文件操作函数简易模拟实现
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <assert.h> #include <stdlib.h> struct MyFILE_{ int fd; char buffer[1024]; int end; //当前缓冲区的结尾 }; typedef struct MyFILE_ MyFILE; MyFILE *fopen_(const char *pathname, const char *mode) { assert(pathname); assert(mode); MyFILE *fp = NULL; if(strcmp(mode, "r") == 0) { } else if(strcmp(mode, "r+") == 0) { } else if(strcmp(mode, "w") == 0) { int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666); if(fd >= 0) { fp = (MyFILE*)malloc(sizeof(MyFILE)); memset(fp, 0, sizeof(MyFILE)); fp->fd = fd; } } else if(strcmp(mode, "w+") == 0) { } else if(strcmp(mode, "a") == 0) { } else if(strcmp(mode, "a+") == 0) { } else{} return fp; } void fputs_(const char *message, MyFILE *fp) { assert(message); assert(fp); strcpy(fp->buffer+fp->end, message); fp->end += strlen(message); printf("%s\n", fp->buffer); if(fp->fd == 0) { //标准输入 } else if(fp->fd == 1) { //标准输出 if(fp->buffer[fp->end-1] =='\n' ) { write(fp->fd, fp->buffer, fp->end); fp->end = 0; } } else if(fp->fd == 2) { //标准错误 } else { //其他文件 } } void fflush_(MyFILE *fp) { assert(fp); if(fp->end != 0) { //把数据写到了内核 write(fp->fd, fp->buffer, fp->end); syncfs(fp->fd); //将数据写入到磁盘 fp->end = 0; } } void fclose_(MyFILE *fp) { assert(fp); fflush_(fp); close(fp->fd); free(fp); }
fopen_
:
- 这个函数模拟了 C 标准库中的
fopen
函数。根据给定的文件路径和打开模式,创建并返回一个MyFILE
结构体指针。根据模式不同,可以选择以只写、只读等方式打开文件。 MyFILE
结构体包含了一个文件描述符fd
,一个缓冲区buffer
,以及end
表示当前缓冲区的结尾位置。- 当以写入模式打开文件时,会调用系统的
open
函数,分配并初始化一个MyFILE
结构体,用于后续的文件写入。
fputs_
:
- 这个函数模拟了 C 标准库中的
fputs
函数。它将给定的字符串写入到MyFILE
结构体的缓冲区中,然后根据文件描述符的不同,选择是否将缓冲区中的数据写入文件。 - 在写入标准输出时,会检查缓冲区的内容,如果末尾是换行符,则执行实际的写入操作,并清空缓冲区。
fflush_
:
- 这个函数模拟了 C 标准库中的
fflush
函数。它将MyFILE
结构体缓冲区中的数据写入文件,并使用syncfs
函数将数据同步到磁盘。
fclose_
:
- 这个函数模拟了 C 标准库中的
fclose
函数。它首先调用fflush_
函数,确保缓冲区数据写入文件,然后关闭文件描述符,并释放分配的MyFILE
结构体内存。
需要注意的是,这段代码是一个简化版本的模拟,实际的 C 标准库文件操作更加复杂,并且在实际应用中会涉及更多的细节和错误处理。此代码示例提供了一个简单的思路,用于理解文件操作的基本原理。
重定向
看下面的代码
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> int main() { close(1); int fd = open("myfile", O_WRONLY|O_CREAT, 00644); if(fd < 0){ perror("open"); return 1; } printf("fd: %d\n", fd); fflush(stdout); close(fd); exit(0); }
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢?
dup2 系统调用
在 Linux 中,dup2
是一个系统调用,用于创建一个文件描述符的副本,并将副本连接到另一个文件描述符。它的原型如下:
int dup2(int oldfd, int newfd);
其中,oldfd
是现有的文件描述符,而 newfd
是你想要创建的新文件描述符。调用 dup2(oldfd, newfd)
会将 newfd
关联到 oldfd
所指向的文件,就像 newfd
是通过 open
或其他方式创建的一样。
具体来说,dup2
调用的作用是将文件描述符 newfd
关闭(如果 newfd
已经打开),然后复制 oldfd
的所有属性(包括文件状态标志、文件偏移量等),最终将 newfd
与 oldfd
指向的文件相连接。这意味着对于 newfd
的任何读取或写入操作都会影响到与 oldfd
相关联的文件。
dup2
的典型用途之一是重定向标准输入、标准输出或标准错误流。通过将某个文件描述符与标准输入、标准输出或标准错误的文件描述符(0、1、2)连接,可以实现输入输出的重定向。
例如,以下代码片段将标准输出重定向到一个文件:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("open"); return 1; } // 使用 dup2 将文件描述符 1(标准输出)重定向到 fd if (dup2(fd, 1) < 0) { perror("dup2"); return 1; } // 现在标准输出将写入到 output.txt 文件 printf("This will be written to output.txt\n"); close(fd); return 0; }
这段代码中,dup2(fd, 1)
将文件描述符 1(标准输出)重定向到 fd
,使得后续的标准输出都会写入到 “output.txt” 文件中。
在minishell中添加重定向功能
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <sys/wait.h> // 添加头文件以支持 waitpid 函数 #define MAX_CMD 1024 char command[MAX_CMD]; // 获取用户输入命令 int do_face() { memset(command, 0x00, MAX_CMD); printf("minishell$ "); fflush(stdout); // 使用 scanf 读取用户输入,遇到换行符为止 if (scanf("%[^\n]%*c", command) == 0) { getchar(); return -1; } return 0; } // 解析命令行输入,将输入命令分解成参数列表 char **do_parse(char *buff) { int argc = 0; static char *argv[32]; // 最多支持 32 个参数 char *ptr = buff; while (*ptr != '\0') { if (!isspace(*ptr)) { argv[argc++] = ptr; while ((!isspace(*ptr)) && (*ptr) != '\0') { ptr++; } } else { while (isspace(*ptr)) { *ptr = '\0'; // 将空白字符替换为字符串结束符 ptr++; } } } argv[argc] = NULL; // 参数列表以 NULL 结尾 return argv; } // 处理重定向操作 int do_redirect(char *buff) { char *ptr = buff, *file = NULL; int type = 0, fd, redirect_type = -1; while (*ptr != '\0') { if (*ptr == '>') { *ptr++ = '\0'; redirect_type++; if (*ptr == '>') { *ptr++ = '\0'; redirect_type++; } while (isspace(*ptr)) { ptr++; } file = ptr; while ((!isspace(*ptr)) && *ptr != '\0') { ptr++; } *ptr = '\0'; if (redirect_type == 0) { fd = open(file, O_CREAT | O_TRUNC | O_WRONLY, 0664); } else { fd = open(file, O_CREAT | O_APPEND | O_WRONLY, 0664); } dup2(fd, 1); // 将标准输出重定向到文件 } ptr++; } return 0; } // 执行命令 int do_exec(char *buff) { char **argv = {NULL}; int pid = fork(); // 创建子进程 if (pid == 0) { // 子进程中执行命令 do_redirect(buff); argv = do_parse(buff); if (argv[0] == NULL) { exit(-1); } execvp(argv[0], argv); // 执行命令 } else { // 父进程等待子进程执行结束 waitpid(pid, NULL, 0); } return 0; } int main(int argc, char *argv[]) { while (1) { if (do_face() < 0) continue; do_exec(command); // 执行用户输入的命令 } return 0; }
这段代码实现了一个基本的交互式命令行解释器(shell)。它允许用户输入命令,并在子进程中执行这些命令。以下是各个函数的功能解释:
do_face()
函数:
- 该函数用于显示命令提示符,读取用户输入的命令。
- 使用
scanf
函数读取用户输入的一行命令,并将其存储在command
缓冲区中。
do_parse()
函数:
- 该函数用于解析命令行输入,将输入命令分解成参数列表。
- 通过遍历输入的字符,将非空白字符作为参数的起始位置,并将参数分割为单独的字符串。
- 参数列表会存储在
argv
数组中,每个元素都指向一个参数字符串,最后一个元素为NULL
。
do_redirect()
函数:
- 该函数用于处理重定向操作,将标准输出重定向到指定文件。
- 在命令字符串中寻找
>
符号,根据符号后的内容判断重定向的类型和目标文件名,然后使用文件操作函数打开该文件并将标准输出重定向到该文件。
do_exec()
函数:
- 该函数用于执行解析后的命令。
- 使用
fork
创建子进程,子进程中调用do_redirect()
进行重定向,然后使用execvp()
函数执行命令。
main()
函数:
- 主函数使用一个无限循环,等待用户输入命令并执行。
- 调用
do_face()
获取用户输入,并在do_exec()
中执行命令。
这个简化的 minishell
支持基本的命令执行和标准输出重定向。它通过 fork
和 execvp
实现命令的执行,同时通过重定向实现标准输出的重定向。