目录
基础IO(文件的输入输出操作)
FILE *fopen(char* filename,char* mode);(文件名称,打开方式)
size_t fread(char* buf,size_t block_size,size_t block_count,FILE* fp);(缓冲区,块大小,块个数,文件流指针);
size_t fwrite(char*data,size_t block_size,size_t block_count,FILE* fp);(数据首地址,块大小,块个数,文件流指针);
int fseek(FILE* fp,long offset,int whence);(将文件的读写指针从whence位置偏移offset个字节)- - - - -跳转文件读写位置
int fclose(FILE* fp);- - - - - 关闭文件流指针,释放资源
标准的IO接口(都是库函数,而库函数就是对系统调用接口的一层封装)
int open(char* filename,int flag,mode_t mode);
ssize_t write(int fd,char* buf,size_t count);
ssize_t read(int fd,char* buf,size_t len);
off_t lseek(int fd,off_t offset,int whence);
int close(int fd);通过文件描述符关闭文件,释放资源;
int dup2(int oldfd,int newfd);- - - - -描述重定向函数
在minishell中实现>/>>标准输出重定向
文件描述符与文件流指针的关系:
基础IO(文件的输入输出操作)
例:fopen/fwrite/fread/fseek/fclose stdin/stdout/stderr
FILE *fopen(char* filename,char* mode);(文件名称,打开方式)
打开方式:*r(只读); r+(读写);w(只写);w+(读写);a(追加写);a+(追加读写);b(二进制操作)。
追加写:每次写入数据总是写入到文件末尾
r+的读写和w+的读写的区别:r+读写打开文件,若文件不存在则报错;w+读写打开文件,若不存在则创建,若存在则清空原有内容。
a:不仅是追加写,并且文件不存在会创建新文件
b:默认清空如果不指定b,则认为文件是文本操作,加上b则认为是二进制操作;区别在于:有时候一个特殊字符,只是一个字符但是占据两个字节的内存(例:读取一个100字节大小的文件,文本操作最终读取出来的数据,不一定是100个字节)
返回值:返回实际一个FILE*的文件流指针作为文件的操作句柄;失败则返回NULL;
size_t fread(char* buf,size_t block_size,size_t block_count,FILE* fp);(缓冲区,块大小,块个数,文件流指针);
size_t fwrite(char*data,size_t block_size,size_t block_count,FILE* fp);(数据首地址,块大小,块个数,文件流指针);
- 注意:fread/fwrite操作的数据实际大小=块大小*块个数;(例:块大小为10,块个数为2,则需要写入/读取20个字节的数据);
- 返回值:返回实际操作的块个数;(例:读取一个文件size为10,count为2,如果文件大小足够则返回2,但是若文件大小只有16个字节,则返回1,因为第二块没有读满;fread如果读到了文件末尾则会返回0)
- 如果读取1000个字节,块个数为1,文件大小只有512字节,虽然读取了512个数据但是也会返回0
int fseek(FILE* fp,long offset,int whence);(将文件的读写指针从whence位置偏移offset个字节)- - - - -跳转文件读写位置
int fclose(FILE* fp);- - - - - 关闭文件流指针,释放资源
fread/fwrite的块大小一般设定为1,块个数设为想要操作的数据长度
fseek:文件没有数据也可以跳转到读写位置;
对文件数据进行字符串操作是时候要注意文件数据中\0这种数据
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = NULL;
fp = fopen("./test.txt", "r+");
if (fp == NULL) {
perror("fopen error");//打印上一个系统调用接口的使用错误原因
return -1;
}
//fseek跳转读写位置 SEEK_SET-从文件起始偏移 SEEK_CUR-从当前读写位置开偏移
//SEEK_END-从文件末尾偏移
fseek(fp, 10, SEEK_END); //将文件的读写位置偏移到末尾
//sizeof获取的是一块空间的大小 / strlen获取的是字符串的长度遇到\0截止
char buf[] = "It's a fine day\0 today~~\n";
int ret = fwrite(buf, strlen(buf), 1, fp);//(数据,块大小,块个数,流指针);
if (ret == 0) {
perror("fwrite error");
return -1;
}
printf("write ret:%d\n", ret);
fseek(fp, 0, SEEK_SET);
char tmp[1024] = {0};
ret = fread(tmp, 1, 1023, fp);//在块大小为1情况下不会出现读取到数据依然返回0的情况
if (ret == 0) {
printf("have no data or error\n");
}
printf("ret:%d-[%s]\n", ret, tmp);
fclose(fp);
return 0;
}
标准的IO接口(都是库函数,而库函数就是对系统调用接口的一层封装)
系统调用IO接口的学习:open\read\write\seek\close
int open(char* filename,int flag,mode_t mode);
filename:要打开的文件名称;
flag:选项参数,文件的打开方式,必选项/可选项
- 必选项(只能选择其一):O_WRONLY(只写);O_RDWR(读写);O_TRUNC(打开文件的同时清空原有内容);O_APPEND(追加写,总是将数据写入到文件末尾)。
- mode:权限,如果使用了O_CREAT有可能创建新文件,就一定要指定文件权限,八进制数字形式。
- 返回值:一个非负数,文件描述符,文件的操作句柄;失败返回-1;
ssize_t write(int fd,char* buf,size_t count);
fd:open返回的文件描述符,文件操作句柄,通过这个fd指定要往那个文件写入数据
buf:要写入文件的数据的空间首地址
count:要写入的数据大小
返回值:返回实际写入文件的数据字节长度;失败则返回-1
ssize_t read(int fd,char* buf,size_t len);
fd:open返回的文件描述符,文件的操作句柄;
buf:从文件中读取数据放到哪个缓冲区中的首地址;
len:想要读取的数据长度,注意这个len不能大于缓冲区的大小;
返回值:返回的是实际读取到的数据字节长度,错误返回-1;
off_t lseek(int fd,off_t offset,int whence);
fd:open返回的文件描述符;
offset:偏移量;
whence:从哪开始偏移;SEEK_SET(文件起始位置),SEEK_CUR(文件当前读写位置),SEEK_END(文件末尾);
返回值:成功返回当前位置相对于起始位置的偏移量;失败返回-1;
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fd = -1;
umask(0);//将当前进程的文件创建权限掩码设置位0-仅当前进程有效
fd = open("./test.txt", O_RDWR|O_CREAT, 0777);
if (fd < 0) {
perror("open error");
return -1;
}
lseek(fd, 10, SEEK_END);// 目的读写位置跳转到文件末尾
char ptr[1024] = "hello world~\n";
int ret = write(fd, ptr, strlen(ptr));
if (ret < 0) {
perror("write error");
return -1;
}
printf("ret:%d\n", ret);
lseek(fd, 0, SEEK_SET);// 跳转到文件的起始位置
char buf[1024] = {0};
ret = read(fd, buf, 1024);
if (ret < 0) {
perror("read error");
return -1;
}
printf("ret:%d-[%s]\n", ret, buf);
close(fd);
return 0;
}
int close(int fd);通过文件描述符关闭文件,释放资源;
为什么打开一个文件,如果不操作一定要关闭,释放资源?
答:因为文件描述符实际上是有限的,若不关闭文件,文件描述符用光,则在进程中就打不开新文件了。
一个进程运行起来,进程会默认打开三个文件:标准输入 0-stdin/标准输出 1-stdout/标准错误 2-stderr;
文件描述符的分配原则:最小未使用;
print打印数据到标准输出,close(1)是把标准输出关闭了;打开新文件后,printf并没有把数据打印出来,而是在刷新缓冲区之后,两数据写入到了文件中。
printf并非帧的一定要把数据写入标准输出文件,而是因为printf函数中操作文件的时候操作的描述符是1。原本向1中写入数据就是向标准输出写入,然后当1指向了新的文件后,这个printf就会将数据写入到新的文件中重定向:将数据不再写入原本的文件,而是写入新的指定的文件中,实现方式就是替换这个描述符对应的文件描述信息。实际上是描述符的重定向,改变描述符所指向的文件,就是改变了数据的流向。
int dup2(int oldfd,int newfd);- - - - -描述重定向函数
功能:让newfd这个描述符也指向oldfd所指向的文件,这时候oldfd和newfd都能够操作oldfd所指向的文件。
**重定向实现原理**:每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//close(1);//关闭0号描述符-就是关闭了标准输入
/*
umask(0);
int fd = open("./test.txt", O_RDWR|O_CREAT, 0664);
if (fd < 0) {
perror("open error");
return -1;
}
dup2(fd, 1);//将1重定向到test.txt这个文件
printf("fd=%d\n", fd);
fflush(stdout);//刷新标准输出缓冲区
close(fd);
*/
FILE *fp = fopen("./test.txt", "r+");
fp->_fileno = 1;//将文件流指针中的文件描述符改成标准输出的描述符了
fwrite("hello world\n", 1, 12, fp);
fclose(fp);
}
在minishell中实现>/>>标准输出重定向
>清空重定向open(O_CREAT|O_TRUNC);
>>追加重定向opend(O_CREAT|O_APPEND);
a.txt fd = open(a.txt);
dup2(fd,1); //子进程在运行指令的时候,ls本身要将数据写入标准输出
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
while(1) {
//增加一个shell提示
printf("[san@minishell]$ ");
fflush(stdout);//刷新标准输出缓冲区
//1. 等待标准输入
char buf[1024] = {0};
fgets(buf, 1023, stdin);
buf[strlen(buf)-1] = '\0'; //buf[...]='\n'
//1.5 解析重定向
//ls -l > a.txt
char *ptr = buf;
int redirect_flag = 0;
char *redirect_file = NULL;
while(*ptr != '\0') {
if (*ptr == '>') {
redirect_flag = 1;//这是清空重定向
*ptr = '\0';//将>替换成结尾标志,则命令的解析到此位置就完毕了
ptr++;
if (*ptr == '>') {//有第二个>则是追加重定向
redirect_flag = 2;
*ptr = '\0';
ptr++;
}
while(*ptr == ' ' && *ptr != '\0') ptr++;//将a.txt之前的空格走完
redirect_file = ptr;//redirect_file这个指针指向了a.txt中a的位置
while(*ptr != ' ' && *ptr != '\0') ptr++; // 将a.txt字符走完
*ptr = '\0';
}
ptr++;
}
//2. 对输入命令数据进行解析
char *argv[32] = {NULL};
int argc = 0;
ptr = buf;
// [ ls -a -l ]
while(*ptr != '\0') {
if (*ptr != ' ') {
argv[argc] = ptr;
argc++;
while(*ptr != ' ' && *ptr != '\0') {
ptr++;
}
*ptr = '\0';
}
ptr++;
}
argv[argc] = NULL;//最后一个参数的下一个位置置NULL
//3. 创建子进程 4. 在子进程中程序替换
pid_t pid = fork();
if (pid == 0) {
if (redirect_flag == 1) {//清空重定向
int fd = open(redirect_file, O_WRONLY|O_CREAT|O_TRUNC, 0664);
dup2(fd, 1);//将标准输入重定向到redirect_file;原本要打印的数据就会被写入文件
}else if (redirect_flag == 2){ // 追加重定向
int fd = open(redirect_file, O_WRONLY|O_CREAT|O_APPEND, 0664);
dup2(fd, 1);//将标准输入重定向到redirect_file;原本要打印的数据就会被写入文件
}
//execvp(char *file, char *argv[]) file--新程序名称
execvp(argv[0], (char**)argv);//程序替换成功就去运行新程序了,32行以后就不会运行了
//能够走到第33行,那么肯定程序替换失败了
perror("execvp error");//打印上一次系统调用接口使用的错误原因
exit(0);
}
//5. 进程等待
wait(NULL);
}
return 0;
}
文件描述符与文件流指针的关系:
文件描述符:是一个非负整数,系统调用的IO接口;在进程中每打开一个文件,都会创建有相应的文件描述信息struct file,这个描述信息被添加在pcb的struct files_struct中,以数组的形式进行管理,随即向用户返回数组的下标作为文件描述符,用于操作文件。
文件流指针:FILE结构体,typedef struct _IO_FILE FILE - - - - -库函数IO接口的操作句柄
通过文件流指针进行最终文件操作的时候,依然还要能够找到文件对应的文件描述符才可以,文件流指针是一个结构体,结构体中有很多的成员变量,其中有一个叫做_fileno- - -这就是文件描述符;
向文件写入数据,并不会直接写入文件,而是先写入缓冲区中,刷新缓冲区的时候才会写入文件;(只有库函数才存在这个缓冲区)。
系统调用接口是直接将数据写入文件的,系统调用接口是没有这个缓冲区的。
exit( )退出会刷新缓冲区,而_exit( )退出时不会刷新缓冲区。