linux操作系统下的I/O
I/O其实就是input和output的简称,它代表着输入和输出。当然,输入输出包括在控制台进行输入输出,也包括着对文件进行输入输出。
标准I/O
标准IO是由ANSI C标准定义的,直观感受就是在引入头文件时,使用的是#include<stdio.h>
可以通过缓冲机制来减少系统调用,实现更高的效率(系统调用:对文件的操作,都是先通过编程语言调用系统的接口,再由系统接口对底层硬件进行操作。)。同时,标准IO是有缓存机制的(首先会通过系统调用读取一批数据到缓冲区,应用先是从缓冲区中进行数据读取的,只有当缓冲区已空时,才会进行系统调用来再读取一批数据)
文件类型
在linux系统下,文件类型可以被分为
常规文件 -> r
普通文件 -> -
目录文件 -> d
字符设备文件 -> c
块设备文件 -> b
管道文件 -> p
套接字文件 -> s
符号链接文件 -> |
标准IO-流
FILE 是一个结构体类型,用来存放打开的文件相关信息,标准IO的所有操作都是基于FILE进行的。
流(stream),FILE又被称为流,可以分为文本流和二进制流。
流的缓冲类型
全缓冲:缓冲区中无数据或无空间或者关闭流时才执行实际的IO操作(默认打开的文件都属于此类)
行缓冲:输入输出中遇到换行符时,进行IO操作(流与终端关联时就是典型的行缓冲)
无缓冲:不进行缓冲,数据直接读写。
当我们打开一个FILE流指针时,标准IO会预定义并且打开3个流:标准输入流,标准输出流,标准错误流。
流的打开和关闭
FILE* fp;
if((fp = fopen("filepath", "打开方式"))==NULL);//打开失败会返回NULL,因此要进行判断
{
perror("fopen");//由perror对打开文件失败进行说明
return -1;
}
//文件操作
fclose(fp);//当流关闭时,会自动刷新缓冲区中的数据,并且释放缓冲区
错误信息处理
extern int errno;//存放错误编号
char* strerror(int errno);//根据错误编号返回对应的错误信息
void perror(const char*s);//先输出字符串,再输出错误号对应的信息
//错误信息处理1
FILE* fp
if((fp = fopen("filepath", "打开方式"))==NULL);//打开失败会返回NULL,因此要进行判断
{
perror("fopen");//由perror对打开文件失败进行说明
return -1;
}
//文件操作
fclose(fp);
//错误信息处理2
FILE* fp
if((fp = fopen("filepath", "打开方式"))==NULL);//打开失败会返回NULL,因此要进行判断
{
printf("fopen:%s\n", strerror(errno));
return -1;
}
//文件操作
fclose(fp);
按字符输入输出
读写一个字符:fgetc,fputc, getc,putc
读写一个标准输入输出字符:getchar, puchar
利用fgetc和fputc实现文件复制
#include<stdio.h>
int main()
{
FILE* fps, *fpd;
int ch;
if((fps = fopen("source.txt", "r"))==NULL)//打开源文件
{
perror("fopen fps");
return -1;
}
if((fpd = fopen("dest.txt", "r+"))==NULL)//打开目标文件
{
perror("fopen fpd");
return -1;
}
while((ch = fgetc(fps)) != EOF)//判断是否到文件末尾
{
fputc(ch, fpd);
}
fclose(fps);
fclose(fpd);
return 0;
}
按行输入输出
s为缓冲区,size一般为缓冲区的大小,从流中读取数据时,若遇到换行符,会在换行符后追加’\0’,不再向后读取。若一直没有遇到换行符,缓冲区中会存储size-1个字符,最后一个位置追加’\0’.
char* fgets(char* s, int size, FILE* stream);
统计文本文件包含多少行
#include<stdio.h>
int main()
{
char buf[6];
FILE* fp;
int line = 0;
int ch;
if((fp = fopen("filename", "r")) == NULL)
{
perror("fopen");
return -1;
}
while(fgets(buf, 6, fp) != NULL)
{
if(buf[strlen(buf)-1] == '\n')
{
line++;
}
}
printf("%d ", line);
fclose(fp);
}
按对象读写
fread,fwrite
FILE* fp;
fp = fopen("student.txt", "w");
struct student
{
char name[10];
int age;
float sorce;
}s[] = {{zhangsan, 18, 89.9},{lisi, 19, 90.2}};
fwrite(s, sizeof(struct student), 2, fp);
fclose(fp);
实现文件复制
#include<stdio.h>
#define N 64
int main()
{
FILE* fps,*fpd;
char buf[N];
int n;
if((fps = fopen("source.txt", "r")) == NULL)
{
perror("fopen fps");
return -1;
}
if((fpd = fopen("dest.txt", "r")) == NULL)
{
perror("fopen fpd");
return -1;
}
while((n = fread(buf, 1, N, fps)) > 0)//fread读取的返回值为读取元素个数
{
fwrite(buf, 1, n, fpd);//将读取的元素个数写入到fpd流中
}
fclose(fps);
fclose(fpd);
return 0;
}
流的刷新和定位
流的刷新
当缓冲区满时,或者遇到换行符时,流的缓冲区会刷新->写到实际的文件中
当关闭流时,缓冲区会刷新->写到实际的文件中
通过fflush函数,强制刷新缓冲区
int fflush(FILE* fp);成功返回0,失败返回-1,在Linux下,只能刷新输出缓冲区
流的定位
当一个流打开的时候,内部有一个读写位置pos,我们在对流进行定位时,实际上就是在定位这个pos
*打开流时,pos=0
*每当读写一个位置,pos会自动加1
ftell(FILE* stream);//成功时,返回流当前的读写位置
fseek(FILE* stream, long offset, int whence);//定位到流的某个位置
rewind(FILE* stream);//定位到流的起始位置
//whence为基准位置,参数有:SEEK_SET/SEEK_CUR/SEEK_END(分别代表文件起始,当前,末尾)
//offset为偏移量。
格式化输出
例:以指定格式年-月-日写入文件和缓冲区
int year, month, day;
FILE* fp;
char buf[64];
year = 2023;month = 1;day = 19;
fp = fopen("test.txt", "a+");
fprintf(fp, "%d-%d-%d\n", year, month, day);
sprintf(buf, "%d-%d-%d\n", year, month,day);
例:每隔1秒向文件test.txt写入系统当前时间
1,2022-12-11 15:21:31
2, 2022-12-11 15:21:32
无线循环,直到按ctrl+C结束,每次执行程序,系统时间追加到文件末尾,序号递增
#include<stdio.h>
#include<unistd.h> //sleep
#include<time.h> //time localtime
#include<string.h>
int main()
{
FILE* fp;
int line = 0;
char buf[64];
time_t t;
struct tm* tp;
if((fp = fopen("timetofile.txt", "a+"))==NULL)//打开文件fp
{
perror("fopen");
return -1;
}
while(fgets(buf, 64, fp) != NULL)//计算当前行数
{
if(buf[strlen(buf)-1] == '\n') line++;
}
while(1)
{
time(&t);
tp = localtime(&t);//得到当前时间
fprintf(fp, "%02d, %d-%02d-%02d %02d:%02d:%02d\n", ++line, tp->tm_year+1900, tp->tm_mon+1,
tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec);//按规定格式将tp写入fp流
fflush(fp);//强制刷新缓冲区
sleep(1);//延时1秒
}
return 0;
}
文件I/O
标准IO与文件IO的区别
上面我们说到,标准IO遵循的是ANSI C的标准,明显的特点是头文件#include<stdio.h>
而文件IO遵循的是POSIX标准,头文件为#include<unistd.h>
,除此之外,文件IO并没有缓冲机制,并且是通过一个文件描述符来表示一个打开的文件。可以访问linux中所有类型的文件。
文件描述符
每打开一个文件,都有一个对应的文件描述符,它是一个非负整数,从0开始,依次分配,各个程序的文件描述符互不影响。
open函数
int open(const char* path, int oflag, ...)//oflag为打开方式,
//新建文件会使用第三个参数,指定文件权限,
//打开成功返回的是对应的文件描述符,失败返回EOF
例:只写方式打开1.txt,如果不存在则创建,如果存在则清空
int fd;
if((fd = open("1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0)
{
perror("open");
return -1;
}
例:读写方式打开1.txt, 不存在则创建,存在则报错
int fd;
if((fd = open("1.txt", O_RDWR|O_CREAT|O_EXCL, 0666)) < 0)
{
if(errno == EEXIST)
perror("exist error");
else
perror("other error");
}
close函数
与fclose相同,int close(int fd);成功返回0,失败返回-1
read函数
ssize_t read(int fd, void *buf, size_t count);//从fd中读取数据,放到buf中,放count个
读取成功返回读取的字节数,出错返回EOF,读到文件末尾返回0
用read从指定文件中读取内容,并统计大小
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<fcntl.h>
4 int main()
5 {
6 int fd, total,n;
7 char buf[64];
8 if((fd = open("1.txt", O_RDONLY )) < 0)
9 {
10 perror("open");
11 return -1;
12 }
13
14 while((n = read(fd, buf, 64)) > 0)
15 {
W> 16 total += n;
17 }
18 printf("total = %d\n", total);
19 close(fd);
20 return 0;
21 }
write函数
ssize_t write(int fd, void *buf, size_t count);//向fd文件中写入,通过buf写入count个字节
//成功写入返回写入字节数,出错返回EOF
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
int main()
{
int fd;
char buf[20];
if((fd = open("1.txt", O_WRONLY|O_CREAT|O_TRUNC)) < 0)
{
perror("open");
return -1;
}
while(fgets(buf, 20, stdin)!=NULL)
{
if(strcmp(buf, "quit\n")==0) break;
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
lseek函数
lseek函数与fseek函数非常相似,仅仅是第一个变量由FILE流换为文件描述符
用文件IO实现文件的复制
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<errno.h>
#define N 64
int main()
{
int fds, fdt;
int n;
char buf[N];
if((fds = open("1.txt", O_RDONLY)) == -1)
{
fprintf(stderr, "fds open %s\n", strerror(errno));
}
if((fdt = open("2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666)) == -1)
{
fprintf(stderr, "fdt open %s\n", strerror(errno));
}
while((n = read(fds, buf, N)) > 0)
{
write(fdt, buf, n);
}
close(fds);
close(fdt);
return 0;
}
读取目录
打印指定目录下的所有文件名称
#include<stdio.h>
#include<unistd.h>
int main()
{
DIR* dirp;
struct dirent* dp;
if((dirp = opendir("opendir")) == NULL)
{
perror("opendir");
return -1;
}
while((dp = readdir(dirp)) != NULL)
{
printf("%s\n" , dp->d_name);
}
closedir(dirp);
return 0;
}
修改文件权限chmod
首先,我们输入ll可以显示当前目录下文件的详细信息,如下图所示:
第一个红色的框表示当前文件类型,-表示普通文件
第二个框表示当前文件的读写权限(针对文件所有者来说),rwx分别表示可读权限,可写权限,可执行权限。-表示没有权限
第三个框表示相同意思(但是针对文件所属组的其他用户来说的)
第四个框表示也是相同意思(针对组外用户来说的),第四和第五个框之间的1表示文件的硬链接计数
第五个框和第六个框表示文件所属用户和所属组
第七个框表示文件大小(k为单位)
最后两个框表示的是文件最后修改的时间和文件名称
那么主要针对文件权限来说,
我们可以这样来看,比如针对文件所有者来说,看第一个框,如果r的位置为-,那么对应二进制为0,r位置不为-,对应二进制为1,以此类推,可以产生3位二进制位,而这样的表示刚好可以由一个8进制数组代替,从而也叫8进制表现形式,因此三个框框,共有3个8进制数字来表示其所有的权限。
那么对于文件的权限我们是怎样进行修改的呢?
chmod函数
chmod u+r 1.txt 表示给文件1.txt的所有者增加可读权限
chmod g-w 1.txt 表示给文件1.txt的组内用户减去可写权限
chmod o+x 1.txt 表示给文件1.txt的其他用户增加可执行权限
这样就可以修改文件的权限了。
但是为了防止由于某个用户拥有了当前目录的可写权限后,可以删除目录下由其他用户所创建的文件我们引入粘滞位(注:拥有目录的可写权限后就可以对目录下的文件进行增加和删除)
粘滞位
为了解决上述的问题,我们使用chmod +t <filename>
这样的操作,使得即使拥有目录的可写权限后,也无法删除由其他用户创建的文件。(注:要在root用户下才能对文件增加或者删除沾滞位)
对于这个ds文件而言,它的有可写权限(针对其他用户而言),这样会导致我在这个目录下创建的文件可以被其他用户删除修改,这样显然是不科学的。因此我们需要设置沾滞位。
chmod +t ds
可以看到已经修改成功了。
有些友友们可能在输入chmod +t <filename>
时出现下面的提示。
这是因为我们修改权限的文件不属于我们这个用户,那么会报没有权限的错误,我们需要使用sodu
来暂时拥有root用户的权限,这样就可以进行修改了。
sudo chmod +t tmp
库的创建和使用
库实际上是一个二进制文件,比如标准c库,线程库…
库一般被存储在lib 或usr/lib中
在linux中,库可以分为静态库和共享库(也被称为动态库)
静态链接库
静态库的特点是:在代码编译(链接)阶段会把静态库中的相关代码复制到可执行文件中。那么在运行时就无需静态库了。因此在程序运行时也无需加载库,运行的速度也会更快,同时也会占用更多的内存。
还有一个重要的点,当静态库发生更新之后,所有使用静态库的代码重新编译(链接)。
静态库的创建
(1)编写静态库源码
#include<stdio.h>
void hello(void)
{
printf("hello world\n");
return ;
}
(2)编译gcc -c hello.c
(3)创建静态库ar crs libhello.a hello.o
(4)可以查看库中包含的函数nm libhello.a
静态库的使用
(5)调用静态库
#include<stdio.h>
void hello(void);//需要进行声明
int main()
{
hello();
return 0;
}
(6)编译gcc test.c -o test -L. -lhello
,注意这里需要增加-L. -lhello
,L.表示要增加的搜索路径为当前路径(因为我们没有将库放到lib这样的默认路径下,系统是找不到这个库的),l后紧跟的是要链接的库的名称。
(7)执行代码./test
此时即使我们删除了libhello.a静态库,程序依然可以运行,就是因为库已经被复制一份了。
动态链接库
与静态库的区别
动态库仅仅记录了需要用到哪个函数,并不会复制库中的相关代码,因此占用内存较少,并且可以同时被多个代码调用,并且更新库后也无需重新编译程序。
动态库的创建
(1)编写库的源码
#include<stdio.h>
void hello(void)
{
printf("hello world\n");
return ;
}
(2)编译生成目标文件gcc -c -fPIC hello.c -Wall
,这样就生成了hello.o
(3)生成hello动态库文件gcc -shared -o libhello.so.1 hello.o
(4)为动态库文件创建符号链接文件ln -s libhello.so.1 libhello.so
动态库的使用
(5)调用动态库(为了省去调用函数时需要进行函数声明,我们可以编写一个头文件,从而调引入头文件)
#include<stdio.h>
#include"hello.h"
int main()
{
hello();
return 0;
}
//hello.h
void hello(void);
(6)编译test.c 并链接动态库gcc test.c -o -L. -lhello
,这里的hello为动态库名,需要和创建的头文件hello.h同名
(7)运行代码./test
若在运行时提示加载动态库错误,我们可以去系统目录下增加一个配置文件
sudo vim /etc/ld.so.conf.d/my_conf
打开之后,将目前动态库的所在路径添加到文件中,即可