区别之------c语言的 文件操作函数(标准IO?)
C语言---文件操作及文件操作函数详解_c语言文件操作函数_羟基与苯的博客-CSDN博客[C语言]文件操作函数_c语言打开文件的函数_衡一兄的博客-CSDN博客
笔记来源:
11-系统编程阶段说再前面的话_哔哩哔哩_bilibili11-系统编程阶段说再前面的话是黑马程序员-Linux系统编程的第47集视频,该合集共计184集,视频收藏或关注UP主,及时了解更多相关视频内容。https://www.bilibili.com/video/BV1KE411q7ee?p=47&vd_source=f443c8140671d1c361aa817ad1193312什么是Linux系统编程?_哔哩哔哩_bilibili什么是Linux系统编程?是【北京迅为】嵌入式学习之Linux系统编程篇的第1集视频,该合集共计31集,视频收藏或关注UP主,及时了解更多相关视频内容。https://www.bilibili.com/video/BV1zV411e7Cy?p=1&vd_source=f443c8140671d1c361aa817ad1193312
概念辨析
系统调用概念
系统调用:内核提供的函数:由操作系统实现并提供给外部应用程序的(为数不多的)编程接口,是应用程序同操作系统之间交互数据的桥梁。
小区别:为了保证系统的安全性,linux手册 manPage中的系统调用都是对系统调用的一次浅封装,比如open()
对应的是sys_open()。因此如果概念想稍微有所区别的话,可以给封装后的调用叫做系统函数。大体上可以混为一谈。就是
操作系统实现并提供给外部应用程序的编程接口。
系统调用和库调用(函数) 两者概念区别:
Linux || 文件描述符 系统调用与库函数_linux系统调用和库函数_布鲁克要补钙的博客-CSDN博客
库函数我们写程序一直在使用,非常多。
文件(系统)IO和标准IO概念,和两者区别
标准IO与系统IO的区别_系统io和标准io的区别-CSDN博客
Linux中的系统IO与标准IO_标准io和系统io的区别-CSDN博客
系统IO的特点
- 系统IO的操作的对象是文件描述符。标准IO的对象是文件指针。
- 系统IO并不带缓冲,因此每次对系统IO的操作都视为数据与内核相交互。
- 某些场合下,只能使用系统IO。例如:处理socket的时候,只能使用系统IO,标准IO不行。
个人对两对概念的理解
系统IO
open
//头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> //包含 flags的宏
//上面三个头文件相当于:
#include <unistd.h> //包含 open函数
// 两个open:第二个open多一个参数,是指定新建不存在文件的权限
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
**入参**:
入参 mode,是一个8进制整型,指定打开不存在文件的权限,只有当参2指定了CREAT才有用。如 0777 (第一个0表示8进制)
ps:创建文件时,指定文件访问权限,权限同时受umask影响:文件权限=mode&(~umask)
入参 flags,打开文件时带的读写权限:
O_RDONLY
O_WRONLY
O_RDWR //读写
O_APPEND //追加属性
O_CREAT //创建属性
O_EXCL
O_TRUNC //截断文件大小(清空)
O_NONBLOCK
**返回值**
一个int,文件描述符;打开失败返回-1并设置,errno,一个操作系统的环境变量,可以直接去查看它的值。
即 ~umask == 775
open的示例:
#include <fcntl.h>
#include <stdio.h>
#include <error.h> //error的头文件
#include <unistd.h>
#include <string.h>//strerror()的头文件
int main(int argc,char* argv[]){
int fd1=0;
int fd2=0;
fd1=open("./dirt.txt",O_RDONLY|O_CREAT|O_TRUNC,0644);
/*打开的文件不存在*/
fd2=open("./dirt2.txt",O_RDONLY);
printf("fd1=%d\n",fd1);
//errno,打开失败会被写值;strerror函数可以解析它的值,以得知打开失败的原因。
printf("fd2=%d,errno=%d:%s\n",fd2,errno,strerror(errno));
close(fd1);
close(fd2);
return 0;
}
close
//头文件
#include <unistd.h>
int close(int fd)
//入参:
open返回的整型文件描述符
//返回值:
成功返回 0;失败 -1;
隐式回收:
实际上,当进程运行结束时, 所有该进程打开的文件会被关闭, 申请的内存空间会被释放, 系统的这一特性称为隐式回收系统资源。但是有时候不能保证进程能即使运行结束,因此手动加close比较保险。
read
打开一个文件后,要对文件进行操作了。 read即把数据读出来到buffer
#include <unistd.h> ssize_t read(int fd, void* buf, size_t count); //ssize_t的第一个s表示 有符号 //参3是缓冲区的大小 //成功返回实际读到的字节数,返回0时意味着读到了文件末尾,失败返回-1并设置errno
(*重点)read返回值分析
write
#include <unistd.h> ssize_t write(int fd, const void* buf, size_t count); //3参注意和read的3参区别开,这个是实际要写入的内容的大小,而非源buffer 的大小 //成功返回实际写入的字节数, 失败返回-1, 并设置errno
open/close/write/read 小练习--实现cp命令:
//mycp.c
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
const int BS = 1024;//读到的数据放到buffer里面,buffer开栈最大1M
int main(int argc, char* argv[]) {
if (argc != 3) {
printf("format: ./mycp a b\n");
exit(1);
}
int fd1 = open(argv[1], O_RDONLY);
int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1 || fd2 == -1) {
perror("open error");
exit(1);
}
char buf[BS];
ssize_t s;
//读一个buffer写一个buffer
while ((s = read(fd1, buf, BS)) > 0) {
ssize_t ret = write(fd2, buf, s);
if (ret != s) {
perror("write error");
exit(1);
}
}
if (s < 0) {
perror("read error");
exit(1);
}
close(fd1);
close(fd2);
return 0;
}
了解小机制:
(1)系统调用和库函数比较:库函数具有预读入和缓输出机制
fgetc()、fputc() 的实际读写过程----函数内部实现不是真的是字节级别的操作(用户态->内核态->驱动磁盘),是存在读入和写出缓冲的。
了解小机制:标准IO,Unbuffered IO是什么?
标准IO (有偷懒的):ANSI C建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准IO库处理很多细节。例如缓存分配,以优化长度执行IO等。
Unbuffered IO (无偷懒的):无缓冲IO。不带缓存指的是,每个read,write都调用内核中的一个系统调用。
标准IO和Unbuffered IO使用的函数:
两者关系 : Unbuffered IO (无偷懒的)的意义: 1、我们知道Linux的传统是Everything is a file, IO函数不仅用于读写常规文件,也用于读写设备,比如终端或网络设备。在读写设备时通常是不希望有缓冲的,例如向代表网络设备的文件写数据就是希望数据通过网络设备发送出去,而不希望只写到缓冲区里就算完事儿了,当网络设备接收到数据时应用程序也希望第一时间被通知到,所以网络编程通常直接调用Unbuffered IO函数。
标准IO Unbuffered IO 打开 fopen open 读 fread ,fgetc,fgets read 写 fwrite,fputc, fputs write 操作位置 fseek,ftell,rewind lseek 关闭 fclose close 也就是说:
————————————————
如果进程A 和 进程B打开同一文件,如果采用系统IO,进程A写到内核I/O缓冲区中的数据从进程B也能及时读到,而标准库的I/O缓冲区则不具有这一特性。
原文链接:https://blog.csdn.net/w903414/article/details/109340230
(2)OS内核绝不会让你逐字节的向Disk上写数据,实际上它维护了一个系统级缓冲,只有当从用户空间过来的数据在该缓冲上写满时,他才会一次性将数据冲刷到Disk上。如下图右侧内核区方框。
(3)当使用系统调用的方法时,要不断的在用户空间和内核空间进行来回切换,这会消耗大量时间。
【Linux】标准IO,Unbuffered IO,文件描述符_unbufferedio-CSDN博客
案例:使用 fgetc()&fputc() 与 使用 read()&write()字节级buffer读写 ,分别实现cp功能, 通过对比cp相同大文件的cp时间,了解 fgetc()、fputc() 的内部机制。
总结:内核缓冲区处理的是内核空间和磁盘之间的数据传递,目的是减少访问磁盘的次数;而用户缓冲区处理的是用户空间和内核空间的数据传递,目的是减少系统调用的次数
系统IO时发生阻塞
从IO read 文件时是否会发生阻塞等待,文件分为两种属性:
常规性文件(无论数据大小,数据是现成的):
阻塞性文件(即/dev目录下,管道、设备、网络设备文件,比如显示/输入等终端设备,tty文件):因为存在数据暂时还没来的可能性
Demo:体验读文件发生阻塞
这个程序读了标准输入文件,然后写到标准输出。执行这个程序后,程序开始阻塞,等待屏幕输入。
直到屏幕有输入,回车。屏幕回显刚才的输入。
思考:为什么程序执行到read那里,就停住了?谁导致的?
(1)文件应该带着一个表示其是否阻塞的属性;
(2)read函数有针对读阻塞性文件的机制,去等待,而不是去返错。
下面通过open函数重新打开标准输入文件,并把它的性质改为非阻塞,然后进行读写
//echo-nonblock.c #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> const int BS = 128; int main(int argc, char* argv[]) { int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open /dev/tty error"); exit(1); } char buf[BS]; memset(buf, 0, BS); ssize_t n; while ((n = read(fd, buf, BS)) < 0) { if (errno != EWOULDBLOCK) { perror("read /dev/tty error"); exit(1); } else { printf("didn't get input, try again\n"); sleep(1); } } write(STDOUT_FILENO, buf, n); close(fd); return 0; }
小说明:当
read
函数返回-1,并且errno=EAGAIN
或EWOULDBLOCK
,说明不是read
失败,而是read
在以非阻塞方式读一个设备文件或网络文件,没读到数据阻塞方式存在的问题也正是后来网络IO中
select
,poll
和epoll
函数存在的原因
fcntl函数
fcntl
函数:改变一个已经打开文件的访问控制属性(之前学习:open函数可以设置新建文件的访问控制权限)
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
将fd设置为非阻塞模式的核心调用:
int flags=fcntl(fd, F_GETFL);
flags|= O_NONBLOCK;//通过或运算,在文件状态flsgs上加上非阻塞属性
int ret=fcntl(fd, F_SETLF, flags);
lseek函数
:修改 文件偏移量属性 的系统IO函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//参数:
offset:偏移量 (字节)
whence:起始偏移位置(SEEK_SET | SEEK_CUR |SEEK_END)
//返回:
成功:从SEEK_SET的偏移量
失败:-1 error
IO读写 使用同一偏移位置:
如下:新write一个文件内容,紧跟着read,什么也read不到。
修改: 如上,将被注释的lseek放开。手动归零读写指针位置。
小技巧-用lseek
获取文件大小:
lseek(fd, 0, SEEK_END);
注意:
lseek只负责偏移读写位置指针,返回值是偏移量,不一定是真正文件大小。
要想使文件大小真正拓展, 必须引起IO操作
//lseek-extend-file.c int main(int argc,char* argv[]){ int fd=open(argv[1],O_RDWR); if(fd==-1){ perror("open error"); exit(1); } /*从文件的结束位置开始,向后偏移110*/ int size=lseek(fd,110,SEEK_END); printf("The file's size:%d\n",size); /*然后写入一个空字符*/ write(fd,"\0",1); close(fd); return 0; }
小技巧-用ftruncate函数扩展文件大小:(注意文件要有 写权限)
stat/lstat函数
作用:获取文件属性(从inode
中获取);
#include <sys/types.h>
#include <sys/stat.h> //注意头文件需要单独加
#include <unistd.h>
int stat(const char* pathname, struct stat* statbuf);
入参:文件路径、查询结果传出
返回值:成功返回0, 失败返回-1并设置errno
/*传出参数:结构体信息(基本就是ll查出来的东西)*/
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
/* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields.For the details before Linux 2.6, see NOTES. */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
使用demo-利用stat
获取文件大小:属性:
//stat-get-file-size.c
int main(int argc, char* argv[]) {
struct stat st;
int ret = stat(argv[1], &st); //忽略了错误处理
printf("%s size = %ld\n", argv[1], st.st_size);
return 0;
}
注意:stat
穿透: 当用stat获取软连接的文件属性时, 会穿透符号连接直接返回软连接指向的本尊的文件属性, vim, cat等命令也有穿透作用(打开软连接文件,内容不是一个路径,而是文件内容本尊)
解决方法: 换lstat
函数(函数用法完全一样,就函数名前面加个l)
ps: readlink -f 软链接文件
,该命令可以实现查看软链接真实文件内容,即 目标绝对路径。
link和unlink函数
之前了解了创建硬链接的命令
ln makefile makefile.hard
:为makefile创建硬连接
现在学习创建和删除硬链接的函数
link
函数:用来创建硬链接的,结合inode知识,实质上就是为已经存在的文件创建目录项dentry
int link(const char *oldpath, const char *newpath);
用法demo-使用link
和unlink
函数实现mv
命令::
mv//移动文件位置或者重命名
int main(int argc, char* argv[]) {
int ret = link(argv[1], argv[2]);
if (ret == -1) {
perror("link error");
exit(1);
}
ret = unlink(argv[1]);
if (ret == -1) {
perror("unlink error");
exit(1);
}
return 0;
}
unlink
函数的特征: 清除文件时, 如果文件的硬连接计数减到了0, 没有dentry
与之对应, 但该文件仍不会马上被释放掉. 要等到所有打开该文件的进程关闭该文件, 系统才会择机将文件彻底释放。
一个demo验证:
//unlink-demo.c
int main(int argc, char* argv[]) {
int fd = 0;
int ret = 0;
char* p = "test of unlink\n";
char* p2 = "after write something\n";
fd = open("temp.txt", O_RDWR | O_TRUNC | O_CREAT, 0644);
if (fd < 0)
perr_exit("open file error");
ret = write(fd, p, strlen(p));
if (ret == -1)
perr_exit("write error");
printf("hello,I'm printf\n");
ret = write(fd, p2, strlen(p2));
if (ret == -1)
perr_exit("write error");
printf("Entry key to continue\n");
p[3] = 'a';
getchar();
close(fd);
ret = unlink("temp.txt");
if (ret == -1)
perr_exit("unlink error");
return 0;
}
但是如果在unlink之前诱发段错误, 程序崩溃, temp.txt就会存活下来. 所以将unlink这一步放到打开文件之后紧接着就unlink掉
虽然文件被unlink掉了,进程还在运行中,用户用cat查看不到磁盘上的对应文件, 但是write函数拿到fd写文件仍可正常写入。
————————————————
原文链接:https://blog.csdn.net/DanielSYC/article/details/118458055
目录操作
目录也是文件,可以用vim打开目录内容:目录的文件内容 是 目录项和inode
目录权限rwx实际意义
目录操作函数 (opendir readdir closedir ---类似于文件操作函数 --位于man手册第3卷,实际上是库函数)
#include <dirent.h>
DIR* opendir(const char* name); /*返回的是一个目录结构体指针*/
int closedir(DIR* dirp);
struct dirent* readdir(DIR* dirp);//dirctory entry
//目录文件下,实际上不仅包括名字和目录项
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
用目录操作函数实现ls的功能:
//readdir-myls.c
int main(int argc, char* argv[]) {
DIR* dirp = opendir(argv[1]); //忽略了错误处理
struct dirent* sdp;
while ((sdp = readdir(dirp)) != NULL) {//隐藏目录项中的 .和..
if (strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) {
continue;
} else {
printf("%s\n", sdp->d_name);
}
}
closedir(dirp);
return 0;
}
从功能上,可以 比ls多一点小功能。可以传参,不仅能打开当前目录,也可以打开传入的目录
无非 ls 是不传参,写死打开当前目录
*章节练习 -实现 ls-R:递归显示目录
个人思路:参照myls,打开并读取(opendir、readddir)目录,可以得到一级目录项。一级目录项中有文件也有目录。我们需要识别目录,并进一步打开(stat获取文件类型)。
关键是递归的实现。
// 针对目录,执行打开/读取/解析目录项的操作
void fetchdir(const char* dir){
char name[PATH_LEN];
struct dirent* sdp;
DIR* dp;
/*打开目录失败*/
if((dp=opendir(dir))==NULL){
fprintf(stderr,"fetchdir:can't open %s\n",dir);
return;
}
/*循环读取内容*/
while((sdp=readdir(dp))!=NULL){
/*遇到当前目录和上一级目录,跳过,否则会陷入死循环*/
if((strcmp(sdp->d_name,".")==0)||(strcmp(sdp->d_name,"..")==0))
continue;
/*路径名是否越界*/
if(strlen(dir)+strlen(sdp->d_name)+2>sizeof(name)){
fprintf(stderr,"fetchdir:name %s %s is too long\n",dir,sdp->d_name);
}else{
/*拼接为一个路径,传给isFile函数*/
sprintf(name,"%s/%s",dir,sdp->d_name);
isFile(name);
}
}
closedir(dp);
}
//判断如果是普通文件,则直接打印;如果是目录,执行打开/读取/递归的操作
void isFile(char* name){
struct stat sbuf;
/*获取文件属性失败*/
if(stat(name,&sbuf)==-1){
fprintf(stderr,"isFile:can't access %s\n",name);
exit(1);
}
/*这是一个目录文件:调用函数fetchdir*/
if((sbuf.st_mode&S_IFMT)==S_IFDIR){
fetchdir(name,isFile);
}
/*不是目录文件:是一个普通文件,打印文件信息*/
printf("%ld\t\t%s\n",sbuf.st_size,name);
}
int main(int argc,char* argv[]){
/*不指定命令行参数*/
if(argc==1)
isFile(".");
else{ //用户给了不止一个参数
while(--argc>0)
isFile(*++argv);
}
return 0;
}
dup/dup2函数
duplicate:复制, 副本
cat makefile > m1
:将cat的结果重定向到m1(此时m1与makefile内容相同)
cat makefile >> m1
:将cat的结果重定向并追加到m1后面(此时m1是双份的makefile)
CSDNhttps://mp.csdn.net/mp_blog/creation/editor/113981796
The dup() system call creates a copy of the file descriptor oldfd, using the lowest-numbered unused file descriptor for the new descriptor.
dup作用:实现了,对oldfd的文件,再新增一个文件描述符指向它。两个文件描述符指向同一个文件。
#include <unistd.h>
int dup(int oldfd); //返回新的文件描述符。新的文件描述符文件是旧的的副本
dup2
作用:把 newfd指向的文件指针改成 oldfd 的。这样针对 newfd的读写操作,实际生效都是针对 oldfd
int dup2(int oldfd, int newfd);
//返回值:执行成功,返回 newfd