Linux系统编程3--文件IO

前话

内核:当前操作系统的核心程序(主要是驱动程序--一个驱动程序唯一对应一个硬件设备,是组成操作系统内核的关键)

操作系统的本质:程序,内核就是操纵系统的核心程序

内核服务于上层应用,与硬件(硬盘、内存等)打交道

系统调用:由内核提供的函数,由操作系统实现并提供给外部应用程序的编程接口,是应用程序与系统之间数据交互的桥梁

接下来学习严格来说是系统函数--在manpage中,为什么说是系统函数?--系统函数是内核函数(真正调用的函数)做了一层浅封装(系统不想让人看到)

接下来举例说明32位操作系统下,c标准函数和系统函数调用的关系:一个helloword如何打印到屏幕

Linux系统编程3--文件IO_第1张图片

32位操作系统中打开一个程序(进程)有0-4G地址区域,自己所编写的函数和库函数是用户级函数,在0-3G位置(用户空间),文件描述符表在4G区域(内核空间)。

Printf(“hello”)是从库函数(libc)的printf中,向下调用了write函数,write函数的作用是完成用户空间到内核空间的一个进入工作,write函数传递给sys_write,sys_write再找相应驱动,通过驱动找相应硬件。

其中write函数是我们后面要学习的系统函数中的一个

都在manpage第二卷 man 2 函数

文件描述符

定义:内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。

同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4

0- STDIN_FILENO 标准输入

1- STDOUT_FILENO 标准输出

2- STDERR_FILENO 标准错误

一个进程可打开1024个文件

Linux系统编程3--文件IO_第2张图片

PCB进程控制块:本质 结构体(有很多成员,其中有一个成员是一个指针,其能够指向文件描述符表,表里头描述的是文件描述符,文件描述符是指向一个文件结构体的指针(指针指向的文件结构体),其其实再往下细究是键值对)。

PCB描述整个进程的消息的(a.out就是一个进程)

注:如果把文件描述符3关闭,那么4会变为3吗

不会,下一个打开的文件描述符会是3

文件描述符了解一下 - 知乎 (zhihu.com)

缓冲区

[Linux系统编程]文件IO(一)_io系统_Windalove的博客-CSDN博客

更细致的,将hello写入到文件1.txt流程

首先fopen打开文件 fwrite写入文本内容

文本内容来到C标准缓冲区

如果缓冲区满了或者满足条件就刷新C标准缓冲区,调用系统函数write进行写

write把要写入的内容写到内核缓冲区

如果内核缓冲区满了或者满足条件就刷新内核缓冲区,系统调用sys_write将缓冲区内容写入到磁盘(补充:有个进程会择机刷新内核缓冲区)

此时如果有进程读取1.txt文件内容,发现内核缓冲区就有这个文件内容,就直接从内核缓冲区读取

为什么要有缓冲区:

定义:缓冲区就是内存里的一块区域,把数据先存内存里,然后一次性写入硬盘中的文件,类似于数据库的批量操作。

好处:减少对硬盘的直接操作,硬盘的执行速度为毫秒级别,内存为纳秒级别。在硬盘直接操作读写效率太低。

内核缓冲区和C标准缓冲区的区别

C语言标准库函数fopen()每打开一个文件时都会对应一个单独一个缓冲区,而内核缓冲区是公用的。

注:不管是内核缓冲区还是c标准缓冲区,大小默认都是4096
而系统函数的缓冲区,可以自定义,但不是用户级缓冲也不是内核缓冲区
c标准缓冲区属于用户级缓冲区

阻塞和非阻塞

阻塞

阻塞、非阻塞: 是设备文件、网络文件的属性。(读常规文件无阻塞概念。)

是程序在等待消息时的状态。

阻塞方式:就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。

非阻塞方式:就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回。

/dev/tty -- 终端文件。

open("/dev/tty",O_RDWR|O_NONBLOCK) --- 设置 /dev/tty 非阻塞状态。(默认为阻塞状态)

传入参数、传出参数、传入传出参数

传入参数:

    1. 指针作为函数参数。

    2. 通常有const关键字修饰。

    3. 指针指向有效区域, 在函数内部做读操作。

传出参数:

    1. 指针作为函数参数。

    2. 在函数调用之前,指针指向的空间可以无意义,但必须有效。

    3. 在函数内部,做写操作。

    4. 函数调用结束后,充当函数返回值。

传入传出参数:

    1. 指针作为函数参数。

    2. 在函数调用之前,指针指向的空间有实际意义。

    3. 在函数内部,先做读操作,后做写操作。

    4. 函数调用结束后,充当函数返回值。

目录项和inode

一个文件主要由:dentry、inode和数据块

        目录项:包括文件名和inode节点号。
        Inode:又称文件索引节点,包含文件的基础信息以及数据块的指针。
        数据块:包含文件的具体内容,保存在磁盘里
Linux系统编程3--文件IO_第3张图片

inode:

本质是结构体,存储文件的属性信息,如:权限、类型、大小、时间、用户(ls -l出的)、盘块位置…也叫做文件属性管理结构,大多数的inode都存储在磁盘上。

少量常用、近期使用的inode会被缓存到内存中。

所谓的删除文件,就是删除inode,但是数据其实还是在硬盘上,以后会覆盖掉。

所以数据恢复就是重新建立inode,指向硬盘

创建硬链接时,就是增加一个一个的目录项(dentry),目录项不同,Inode相同

dentry:

本质依然是结构体,重要成员变量有两个 {文件名,inode,…}

open函数

open基础

man 2 open
Linux系统编程3--文件IO_第4张图片

如果创建一个新文件就使用第二个open,打开一个文件就使用第一个open

成功打开或创建返回一个文件描述符(非负整数),出错返回-1

pathname 文件路径

flags 权限控制

O_RDONLY|O_WRONLY|O_RDWR | O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....

O_RDONLY:只读
O_WRONLY:只写
O_RDWR:读写
O_CREAT:创建一个新的文件
O_APPEND:追加,再往里头写东西
O_EXCL:判断文件是否存在
O_TRUNC:截断文件大小为0,相当于清空

fd = open("./dict.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644);//rw-r--r--
//如果文件存在,把它以只读方式打开,并截断为零;要是文件不存在就进行创建,并指定文件权限是644

mode: 参数3使用的前提,参2指定了 O_CREAT。 取值8进制数,用来描述文件的 访问权限。

rwx 0664

创建文件最终权限 = mode & ~umask(文件默认权限775)

文件权限 = mode & ~umask

open常见错误

  1. 打开文件不存在

  1. 以写方式打开只读文件(权限问题)

  1. 以只写方式打开目录

当open出错时,程序会自动设置errno,可以通过strerror(errno)来查看报错数字的含义

以打开不存在文件为例:

include
include 包含在include
O_RDONLY, O_WRONLY,O_RDWR包含在include
include 是输出的
#include 
#include 
#include 
#include 
#include 

int main (int argc, char *argv[]){
    int fd;
    fd = open("./wife.c",O_RDONLY);

    printf("fd = %d, errno = %d:%s\n", fd, errno, strerror(errno));
    close(fd);
    return 0;
}

执行结果为:

close函数

int close(int fd);

关闭打开文件,一般有open就要有close

read函数

ssize_t read(int fd, void *buf, size_t count);
参数:
        fd:文件描述符

        buf:存数据的缓冲区         
        count:缓冲区大小
返回值:

        0:读到文件末尾。

        成功;    > 0 读到的字节数。

        失败:    -1, 设置 errno

        -1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞
             方式读一个设备文件(网络文件),并且文件无数据。

write函数

ssize_t write(int fd, const void *buf, size_t count);
    参数:
        fd:文件描述符

        buf:待写出数据的缓冲区

        count:数据大小
返回值:

        成功;    写入的字节数。

        失败:    -1, 设置 errno

实现cp

argc和argv的一些知识

int fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664)
//如果文件存在以读写方式打开,并将其截断为零(清空);如果文件不存在就创建文件,并赋予0664的
权限

int mian(int argc, char* argv[])
//char* argv[]声明一个数组argv,该数组保存多个指向char类型的指针,
//char** argv[]声明argv为指向char类型指针的指针
即:一个是数组类型的声明,一个是指针类型的声明
//argc是命令行总的参数个数
//argv[]是argc的参数,其中第0个参数是程序的全名,以后的参数是命令行后面跟的用户输入的参数

比如:

test.c

#include 
 
int main(int argc, char *argv[])
{
    printf("argc is %d\n",argc);
    for(int i=0;i

编译gcc test.c -o test后,执行./test

结果:

argc is 1
argv[0] is: ./argtest

表明在执行这个程序时,输入的参数只有一个,而且这个参数就是执行程序的这个命令

执行./test 123 abc

结果:

argc is 3
argv[0] is: ./test
argv[1] is: 123
argv[2] is: abc

表明程序输入的参数有3个,命令的后面两个用空格分隔的字符串都传给了main函数。

通过argc和argv[ ]我们就可以通过命令向程序传递参数了

程序

#include 
#include 
#include 
#include 
#include 

int main (int argc, char *argv[]){
    int fd1;
    int fd2;
    char buf[1024];//缓冲区
    int n = 0;
    fd1 = open(argv[1],O_RDONLY);
    if(fd1 == -1) {
        perror("open argv[1] error");//错误处理函数
//printf("xxx error: %d\n", errno);也可以
        exit(1);//退出
}
    fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC, 0664);

    while((n = read(fd1, buf, 1024)) != 0){
        write(fd2, buf, n);
    }
    close(fd1);
    close(fd2);
    return 0;
}

make test编译后

./test hello.c hello1.c

hello1.c中与hello.c相同

fcntl修改文件属性

用来改变一个【已经打开】的文件的 访问控制属性

int fcntl (int fd, int cmd, ... /*arg*/ );
int flgs = fcntl(fd,  F_GETFL);//获取文件状态
flgs|= O_NONBLOCK   //非阻塞
fcntl(fd,  F_SETFL, flgs);//设置文件状态
    获取文件状态: F_GETFL
    设置文件状态: F_SETFL

lseek函数

off_t lseek(int fd, off_t offset, int whence);

    参数:
        fd:文件描述符

        offset: 偏移量,就是将读写指针从whence指定位置向后偏移offset个单位(字节)

        whence:起始偏移位置: SEEK_SET/SEEK_CUR/SEEK_END
             SEEK_SET:文件起始位置;
               SEEK_CUR:当前位置;
              SEEK_END:文件末尾位置;
    返回值:

        成功:较起始位置偏移量

        失败:-1 errno

应用场景:

1.文件的“读”、“写”使用同一偏移位置。

2.使用lseek获取文件大小

3. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须引起IO操作

使用 truncate 函数,直接拓展文件。 int ret = truncate("dict.cp", 250);

写一个句子到空白文件,之后调整光标位置,读取刚才写那个文件。如果不调整光标位置,是读取不到内容的,因为读写指针在内容的末尾

代码如下:

1.    #include   
2.    #include   
3.    #include   
4.    #include   
5.    #include   
6.      
7.    int main(void)  
8.    {  
9.        int fd, n;  
10.        char msg[] = "It's a test for lseek\n";  
11.        char ch;  
12.      
13.        fd = open("lseek.txt", O_RDWR|O_CREAT, 0644);  
14.        if(fd < 0){  
15.            perror("open lseek.txt error");  
16.            exit(1);  
17.        }  
18.     
19.        write(fd, msg, strlen(msg));    //使用fd对打开的文件进行写操作,读写位置位于文件结尾处。  
20.      
21.        lseek(fd, 0, SEEK_SET);         //修改文件读写指针位置,位于文件开头。   
22.      
23.        while((n = read(fd, &ch, 1))){  
24.            if(n < 0){  
25.                perror("read error");  
26.                exit(1);  
27.            }  
28.            write(STDOUT_FILENO, &ch, n);   //将文件内容按字节读出,写出到屏幕  
29.        }  
30.      
31.        close(fd);  
32.      
33.        return 0;  
34.    }  

编译后:

用lseek的偏移来读取文件大小

Linux系统编程3--文件IO_第5张图片

结果如下:

Linux系统编程3--文件IO_第6张图片

使用lseek将fcntl.c原本的698字节填充为800字节,差值102字节:

其实比起读取大小的代码,就时修改了个偏移量,然后要引起IO操作,写入一个“$”

Linux系统编程3--文件IO_第7张图片

编译运行:

Linux系统编程3--文件IO_第8张图片

其他函数

stat函数
    获取文件属性,(从inode结构体中获取)
stat会拿到符号链接指向那个文件或目录的属性。
不想穿透符号就用lstat

stat/lstat 函数:

    int stat(const char *path, struct stat *buf);

    参数:
        path: 文件路径

        buf:(传出参数) 存放文件属性,inode结构体指针。

    返回值:

        成功: 0

        失败: -1 errno

    获取文件大小: buf.st_size

    获取文件类型: buf.st_mode

    获取文件权限: buf.st_mode

    符号穿透:stat会。lstat不会。
可以使用stat获取文件大小
link和Unlink隐式回收

硬链接数就是dentry数目
link就是用来创建硬链接的
link可以用来实现mv命令

函数原型:
int link(const char *oldpath, const char *newpath)
用这个来实现mv,用oldpath来创建newpath,完事儿删除oldpath就行。

删除一个链接  int unlink(const char *pathname)

unlink是删除一个文件的目录项dentry,使硬链接数-1

unlink函数的特征:清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放,要等到所有打开文件的进程关闭该文件,系统才会挑时间将该文件释放掉。
目录操作函数:头文件

    DIR * opendir(char *name);
 DIR*是目录结构体指针,类似于文件结构体指针(FILE*)
DIR*其对应于目录结构体,实现方式是无法看到的

    int closedir(DIR *dp);

    struct dirent *readdir(DIR * dp);
  
        struct dirent {

            inode

            char dname[256];
        }

没有写目录操作,因为目录写操作就是创建文件。可以用touch
char dname[256];表明文件名最大是255
用来做重定向,本质就是复制文件描述符:
dup 和 dup2:

    int dup(int oldfd);        文件描述符复制。

        oldfd: 已有文件描述符

        返回:新文件描述符,这个描述符和oldfd指向相同内容。

    int dup2(int oldfd, int newfd); 文件描述符复制,oldfd拷贝给newfd。返回newfd

下一个部分就是进程知识了!!!!!!!!!!!

你可能感兴趣的:(Linux系统编程,linux,服务器)