【Linux】基础IO——库函数与系统调用的关系

引言

打开文件的本质,就是将该文件的属性加载到内存中,OS内部存在大量被打开的文件,所以就需要管理这些文件:
每打开一个文件,都要在OS内创建该文件对象的struct结构体,然后将这些struct file用某种数据结构链接起来,所以在OS内部对文件的管理就转化成了对链表的增删查改。
在这里插入图片描述

文件可以分为两大类:

  1. 磁盘文件:没有被打开,存储在磁盘上
  2. 被打开的文件:被打开了,在内存中创建了对应的数据结构管理文件的属性

文件是被OS打开的,谁让OS打开?
——进程,所以我们所学习的都是 进程 与 被打开文件 的关系(struct task_struct 与 struct file);


1.文件操作语言方案(C的文件操作接口-库函数)

要对文件进行操作,首先要打开文件,打开操作完成后也要关闭文件,下面复习两个接口,fopen与fclose;

打开文件、关闭文件——fopen、fclose

打开文件——fopen
头文件:stdio.h
函数:FILE *fopen(const char *path, const char *mode);
参数
path——打开文件的路径;
mode——打开文件的模式;
返回值:成功返回FILE的指针,失败返回NULL;
若失败会设置错误码errno(表明出错的原因),可以通过perror接收错误码,将错误码转化成错误码描述打印错误原因,也可以通过strerror将错误码转化成错误码字符串;

关闭文件——fclose
头文件:stdio.h
函数:int fclose(FILE *fp);
参数:pf:指向被打开的文件
返回值:成功返回0,失败返回EOF,并设置错误码errno。

打开文件的模式

打开模式含义若指定文件不存在
“r”(只读)为了输入数据,打开一个已经存在的文本文件出错
“w”(只写)为了输出数据,打开一个文本文件创建新文件
“a”(追加)向文本文件尾添加数据创建新文件
“rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输出数据,打开一个二进制文件创建新文件
“ab”(追加)向一个二进制文件尾添加数据出错
“r+”(读写)为了读和写,打开一个文本文件出错
“w+”(读写)为了读和写,建立一个新的文件创建新文件
“a+”(读写)打开一个文件,在文件尾进行读写创建新文件
“rb+”(读写)为了读和写打开一个二进制文件出错
“wb+”(读写)为了读和写,新建一个新的二进制文件创建新文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写创建新文件

解析:
如果fopen以w模式打开文件,写入的规则是:
1.首先将文件清空
2.从重头重新写入
(若只打开文件,不写入就关闭,文件也会被清空)

如果fopen以a模式打开文件,写入的规则是:
不会清空文件,每次写入都是从文件的结尾写入。


写入——fput、printf

1.fputs
打开文件后还需要进行写入操作,写入操作接口有很多,这里复习fputs。

写入——fputs
头文件:stdio.h
函数:int fputs(const char *s, FILE *stream);
参数:将字符串s写入到文件流stream当中。
返回值:成功返回非负数(写入了多少个字符),错误返回EOF;

以一段最简单的代码为例:在当前目录下创建log.txt文件,并写入5条hello world!。

//fputs文件写入
#include <stdio.h>

#define LOG "log.txt"

int main()
{
    FILE *fp = fopen(LOG, "w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    const char *str = "hello world!\n";
    int cnt = 5;
    while(cnt)
    {
        fputs(str, fp);
        cnt--;
    }

    fclose(fp);
    return 0;
}

2.printf、fprintf、sprintf、snprintf

头文件:stdio.h
函数:

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

解析:
printf——默认向显示器打印,很熟悉,不解析。(显示器也是文件,对应标准输出流stdout)
fprintf——指定文件流stream,向指定文件打印;第二个参数format是格式化控制(与printf一样)
sprintf——向缓冲区打印;可以以格式化流的方式,将格式化信息输出到自定义的缓冲区str里。
snprintf——与sprintf一样,多了一个参数size是缓冲区的大小。

注:
Linux下一切皆文件,显示器也是一个文件,所以:若fprintf(stdout, str);则与printf(str);一样向显示器打印。


读取——fgets

fgets

头文件:stdio.h
函数:char *fgets(char *s, int size, FILE *stream);
参数:从特定的文件流stream中,按行读取对应的内容,将读到的内容放入缓冲区s中,size是缓冲区的大小
返回值:成功返回字符串的起始地址,失败返回NULL。

2.文件操作系统方案(系统的文件操作接口-系统调用)

打开文件、关闭文件——open、close

open:

头文件
sys/types.h
sys/stat.h
fcntl.h
函数
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数
pathname——要打开的文件路径+文件名
flags——打开文件对应的选项(比如读、写)
mode——打开文件的权限(文件的权限,rwx,比如0666)
返回值
成功返回文件描述符(file descriptor),失败返回-1;

close:

头文件
unistd.h
函数
int close(int fd);


open的参数flags的理解

1.系统是如何给一个函数传递多个标志位的?
——比如想给一个函数传递多个标志位,肯定不能设置多个形参比如int func(int flag1, int flag2, int flag3)。又因为传入的标志位flag是int型,有32个比特位,我们可以用1个比特位表示1个标志位,一个整数就可以同时传递32个标志位了——位图
一般就是使用下面的这种方式传递标志位:

#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
#define FOUR 0x8
#define FIVE 0x10

void Print(int flag)
{
    if(flag & ONE) printf("ONE\n");
    if(flag & TWO) printf("TWO\n");
    if(flag & THREE) printf("THREE\n");
    if(flag & FOUR) printf("FOUR\n");
    if(flag & FIVE) printf("FIVE\n");
}

int main()
{
	//测试
    Print(ONE);
    Print(TWO);
    Print(THREE);
    Print(FOUR);
    Print(FIVE);
    printf("----------\n");
    Print(ONE|TWO);
    printf("----------\n");
    Print(ONE|TWO|THREE);
    printf("----------\n");
    return 0;
}

结果:
在这里插入图片描述

而我们在查看open的参数flag的时候,发现有很多参数(如下图所示,没有截全),这些其实就是宏值,每个宏对应的数字比特位是不重叠的,每个对应一个选项,所以学习时学习每个的含义就可以了。
在这里插入图片描述

open的参数flag的学习

1.O_CREAT——文件存在就打开,不存在就创建
(一般创建新文件不使用两个参数的open,因为权限会乱码,要使用三个参数的open函数,指定文件权限)
2.O_WRONLY——只写
(O_WRONLY | O_CREAT默认不会对原始文件内容做清空!重复写入的时候会很乱,可以自己尝试)
3.O_TRUNC——对文件内容做清空
4.O_APPEND——追加(没有写入,只是追加)
(追加一般不和清空一起使用,因为逻辑上就有矛盾)
5.O_RDONLY——只读
(读取是对一次已经存在的文件读取,所以使用两个参数的open,不用指定文件权限)

使用例:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
    int fd = open(LOG, O_CREAT | O_WRONLY, 0666);
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    close(fd);
}

在这里插入图片描述
此时发现生成的文件权限不是指定的666,而是664,这是为什么?
——因为存在权限掩码umask,umask的计算:最终权限 = 起始权限 & (~umask),普通用户的umask为0002,所以生成的权限是664。

那么我们要如何不让权限掩码影响我们,直接生成指定权限的文件呢?
——调用umask函数,可以设定当前进程启动时,属于自己的umask。
当该进程生成文件的时候,umask用系统的还是该进程的?
——就近原则,离谁更近就用谁的,所以肯定是用当前进程的umask。

umake:设定进程的权限掩码
头文件
sys/types.h
sys/stat.h
函数
mode_t umask(mode_t mask);
参数:想要设定的权限掩码

//头文件略
int main()
{
    umask(0);
    int fd = open(LOG, O_CREAT | O_WRONLY, 0666);
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    close(fd);
    return 0;
}

此时发现生成的文件权限掩码与自己设定的一致。
在这里插入图片描述

写入——write

头文件:unistd.h
函数:ssize_t write(int fd, const void *buf, size_t count);
参数
fd——文件描述符(open的返回值)
buf——缓冲区
count——写入的字符数
将缓冲区中count大小的数据写入到fd中。
返回值:实际写了多少字节,失败返回-1;

读取——read

头文件:unistd.h
函数:ssize_t read(int fd, void *buf, size_t count);
参数
fd——文件描述符(open的返回值)
buf——缓冲区
count——空间的大小(字节数)
从文件描述符fd中,将数据按数据块的方式读取出来,读取到buf中,读取conut个字节
返回值:读取了多少字节,失败返回-1;

几个系统调用的使用

使用1:
O_WRONLY只写入

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
    umask(0);
    int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);//打开/创建文件+只写+清空
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }

    const char *str = "hello world!";
    int cnt = 5;
    while(cnt)
    {
        char line[128];
        snprintf(line, sizeof(line), "%s, %d\n", str, cnt);
        write(fd, line, strlen(line));//strlen不计算\0,但是这里不+1,因为\0是C语言的规定,不是文件的规定!!
        cnt--;
    }
    close(fd);
    return 0;
}

结果1:
在这里插入图片描述


使用2:
O_WRONLY | O_APPEND追加写入(不带清空)

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
    umask(0);
    int fd = open(LOG, O_CREAT | O_WRONLY | O_APPEND, 0666);//追加写入
    //int fd = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);//打开/创建文件+只写+清空
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }

    const char *str = "hello world!";
    int cnt = 5;
    while(cnt)
    {
        char line[128];
        snprintf(line, sizeof(line), "%s, %d\n", str, cnt);
        write(fd, line, strlen(line));//strlen不计算\0,但是这里不+1,因为\0是C语言的规定,不是文件的规定!!
        cnt--;
    }
    close(fd);
    return 0;
}

结果2:
运行多次,可以发现是追加写入。
在这里插入图片描述


使用3:O_RDONLY读取
(这里读取的就是使用2中生成的文件)

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define LOG "log.txt"

int main()
{
    umask(0);
    int fd = open(LOG, O_RDONLY);//只读
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }

    char buffer[1024];
	//sizeof会计算\0,IO操作一定要注意\0的问题!记得-1
    ssize_t n = read(fd, buffer, sizeof(buffer)-1);
    if(n > 0)
    {
        buffer[n] = '\0';
        printf("%s", buffer);
    }
    close(fd);
    return 0;
}

结果3:
读取成功(我们这里是一个一个读取的,不是和C接口一样按行读取,要实现需要精细化处理)
在这里插入图片描述

3.库函数与系统调用的关系

C语言的接口(库函数)一定调用了系统调用接口,是上下层关系。

在这里插入图片描述
程序员与系统调用接口的关系:
我们平时使用的C、C++库都是对系统调用的封装!

而函数库也是人写的,他们与我们同样不可能绕过OS来对硬件做操作,同时也无法直接操作操作系统,所以只能通过系统调用的方式来操作硬件设备。
在这里插入图片描述
同时其他所有语言都是一样的,都要调用这些系统调用接口,只是封装的形式不同!
在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值