Linux系统编程-文件操作(黑马笔记)

1.区分系统调用和库函数

库函数是我们一开始进行学习就接触到的函数,是c语言中已经包装好的函数,所以这里不多叙述。但是我们要理解系统调用,系统调用是用户进入"内核态"的门(进入内核态 = 让操作系统"亲自出马",帮你完成只有它才能做的底层、敏感、特权操作)。只有使用系统调用,我们写的c语言程序才能真正的进入内核态,用系统提供的函数来完成我们想做的工作。

总结一句话:我们可以通过系统调用进入内核态,并且真正调用内核中提供的函数,这就是系统调用。

系统调用用户程序进入内核 的"门";
库函数在用户空间 帮你"包装"这些门、或额外提供功能的"工具箱"。


📌 具体区别(对照表)

维度 系统调用(System Call) 库函数(Library Function)
所在空间 内核空间 用户空间
调用方式 触发软中断(syscall 指令) 普通函数调用(call 指令)
执行权限 需要切换到内核态(ring 0) 用户态即可(ring 3)
执行速度 慢(用户↔内核切换开销) 快(无上下文切换)
功能范围 直接与硬件、资源交互(如文件、进程、网络) 可以是系统调用的封装,也可以纯计算(如字符串处理)
例子 open, read, write, fork, execve fopen, fread, printf, malloc, strlen
来源 Linux 内核提供 libc(如 glibc)或其他库提供
可移植性 依赖操作系统(Linux 特有) 标准库函数跨平台(如 ANSI C)

✅ 总结一句话:

系统调用是"内核提供的最小功能单元",库函数是"程序员友好的封装或扩展"。

写系统程序时,优先用库函数 (更简单、更安全),需要精细控制或性能时再直接用系统调用

2.open和close函数

所有函数都可以通过查看手册的方式进行学习,不过难度比较大,这里贴出打开手册的方法,供有志者学习。

bash 复制代码
man 2 open

open 函数

int open(char *pathname, int flags);

pathname:要打开的文件路径

flags :文件打开方式

O_RDONLY 只读

O_WRONLY 只写

O_RDWR 读写

O_CREAT 不存在则创建

O_APPEND 追加写

O_TRUNC 清空再写

O_EXCL 必须创建且文件不存在

O_NONBLOCK 非阻塞

返回值

成功:文件描述符(非负整数)

失败:-1,并设置 errno

三参数原型

int open(char *pathname, int flags, mode_t mode);

参数

pathname:文件路径

flags :同上

mode :仅当 flags 含 O_CREAT 时生效(八进制)

例:0664 → rw-rw-r--

最终权限 = mode & ~umask

"最终权限 = mode & ~umask" 就是 "你给的权限先去掉 umask 禁止的位,才是真正落盘的权限。" 这里是二进制,我们用二进制的每一位的0,1来代表是否打开权限。

返回值

成功:文件描述符

失败:-1,并设置 errno

close函数

close 函数

int close(int fd);

demo

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int fd;

    /* 1. 权限:读+写;2. 不存在则创建;3. 若已存在则清空 */
    fd = open("./dict.cp",
              O_RDWR | O_CREAT | O_TRUNC,
              0644);          /* rw-r--r-- */

    /* 4. 必须检查返回值 */
    if (fd == -1) {
        //stderr标准输入流 默认是终端
        fprintf(stderr, "open failed: %s\n", strerror(errno));
        return 1;
    }

    printf("fd = %d\n", fd);

    /* 5. 关闭同样检查错误(虽然极少失败,但养成习惯) */
    if (close(fd) == -1) {
        //错误处理函数
        perror("close");
        return 1;
    }

    return 0;
}

2.1文件描述符:

相当于拿到了进程中文件的唯一id,用这个id去控制文件

2.2预读入和缓输出

  • 预读入提前 把用户还没要的 数据块从磁盘读进页缓存,等你真正 read() 时直接命中内存,磁盘几乎零等待。

  • 缓输出 把用户 write() 的数据写进页缓存 就立即返回,内核稍后 再异步刷盘(或按 fsync/O_SYNC 强制落盘)。

2.3阻塞和非阻塞(设备文件或者网络文件)

ftml函数

复制代码
int fcntl(int fd, int cmd, ... /* arg */ );

参数

  • fd:文件描述符。

  • cmd:命令,指定要执行的操作。

  • arg:命令的参数,具体取决于 cmd 的值。

返回值

  • 成功时,返回执行命令后的结果,这可能因命令不同而有所不同。

  • 失败时,返回 -1,并设置 errno 以指示错误。

常见的命令

  • F_DUPFD:复制文件描述符。这个命令会创建一个新的文件描述符,它与原始文件描述符引用同一个文件。

  • F_GETFD:获取文件描述符的标志。这些标志控制文件描述符的行为,例如是否应该在执行 exec 函数时关闭文件描述符。

  • F_SETFD:设置文件描述符的标志。

  • F_GETFL:获取文件状态标志。这些标志控制文件的打开模式,例如是否为只读或只写。

阻塞(可以简单的理解为文件的一种属性 设备文件和网络文件)

阻塞:阻塞掉CPU等待输入

非阻塞:返回错误信息,不会阻塞。

设置文件的阻塞和非阻塞(一种属性)

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int set_nonblock(int fd, int on) {
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) return -1;

    if (on)
        flags |= O_NONBLOCK;
    else
        flags &= ~O_NONBLOCK;
       //设置flag,成功返回1,否则返回0
    return fcntl(fd, F_SETFL, flags);
}

int main() {
    int fd = open("fifo", O_RDWR);
    if (fd == -1) { perror("open"); return 1; }

    set_nonblock(fd, 1);  /* 设为非阻塞 */
    /* ... 读写 ... */
    set_nonblock(fd, 0);  /* 恢复阻塞 */

    close(fd);
    return 0;
}

2.4使用fcntl函数设置文件描述符

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

int main(int argc, char *argv[])
{
    int fd1 = open(argv[1], O_RDWR);
    printf("fd1 = %d\n", fd1);

    int newfd = fcntl(fd1, F_DUPFD, 0); // 0被占用,fcntl使用文件描述符表中可用的最小文件描述符返回
    printf("newfd = %d\n", newfd);

    int newfd2 = fcntl(fd1, F_DUPFD, 7); // 7,未被占用,返回 >= 7 的文件描述符.
    printf("newfd2 = %d\n", newfd2);

    return 0;
}

3.read和write函数

read 函数

原型:

ssize_t read(int fd, void *buf, size_t count);

参数:

fd 文件描述符

buf 存放读入数据的缓冲区

count 缓冲区大小(最多读多少字节)

返回值:

成功:实际读到的字节数(可能 < count)

失败:-1,并设置 errno

-1:并且errno=BAGIN或EWOULDBLOCK,说明不是read失败,而是被阻塞(设备文件或者网络文件)

write 函数

原型:

ssize_t write(int fd, const void *buf, size_t count);

参数:

fd 文件描述符

buf 待写入数据的缓冲区

count 要写入的字节数

返回值:

成功:实际写入的字节数(可能 < count)

失败:-1,并设置 errno

demo(实现copy效果)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, char *argv[]) 

    int fd1 = open(argv[1], O_RDONLY);
    if (fd1 == -1) { perror("open src"); return 1; }
     //以读写打开,有就截断为空,没有就是create,0664是创建的权限
    int fd2 = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0664);
    if (fd2 == -1) { perror("open dst"); close(fd1); return 1; }

    char buf[1024];           /* 64K 缓冲更快 */
    ssize_t n;
    while ((n = read(fd1, buf, sizeof(buf))) > 0) {
        if (write(fd2, buf, n) != n) { perror("write"); break; }
    }
    if (n == -1) perror("read");

    close(fd1);
    close(fd2);
    return 0;
}

4.lseek函数

lseek 函数(控制光标的偏移)

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

参数:

fd 文件描述符

offset 偏移量(字节数)

whence 偏移基准

SEEK_SET 文件开头

SEEK_CUR 当前位置

SEEK_END 文件末尾

返回值:

成功:返回新的偏移量(相对于文件起始位置的字节数)

失败:-1,并设置 errno

demo(1. 文件的"读"和"写"使用同一偏移位置。

cpp 复制代码
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(void)
{
    int fd, n;
    char msg[] = "It's a test for lseek\n";
    char ch;

    fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open lseek.txt error");
        exit(1);
    }

    write(fd, msg, strlen(msg));
    /* 使用 fd 对打开的文件进行写操作,文件读写位置位于文件结尾处 */

    lseek(fd, 0, SEEK_SET);
    /* 修改文件读写指针位置,位于文件开头。注释该行会怎样呢? */

    while ((n = read(fd, &ch, 1))) {
        if (n < 0) {
            perror("read error");
            exit(1);
        }
        /* 此处缺少输出或处理逻辑,可自行补充 */
    }

    close(fd);
    return 0;
}

demo(2. 使用 lseek 获取文件大小。)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }

    int length = lseek(fd, 0, SEEK_END);
    printf("file size: %d\n", length);

    close(fd);
    return 0;
}

demo(3. 使用 lseek 拓展文件大小:要想使文件大小真正拓展,必须引起一次 IO 操作(如 write 一个字节)

cpp 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }

    int length = lseek(fd, 110, SEEK_END);
    printf("file size:%d\n", length);
    //引起真正的io操作,否则只是简单的偏移。
    write(fd, "a", 1);

    close(fd);
    return 0;
}

5.inode和detry

总结一句话是:inode本身是文件的一种数据结构,他记录了许多文件的属性。dentry是目录项一个目录中有许多dentry,dentry记录了文件名->inode的映射。

inode(索引节点)

  • inode 是文件系统中的一个数据结构,用于存储文件的元数据(metadata),但不包括文件名。

  • 元数据 包括文件的权限、所有者、大小、创建时间、修改时间、访问时间、文件类型(如普通文件、目录、符号链接等)、以及指向文件数据块的指针等。

  • 每个文件在文件系统中都有一个唯一的 inode 号,通过这个 inode 号可以找到文件的 inode,进而访问文件的元数据和数据块。

dentry(目录项)

  • dentry 是目录中的一个条目,它将文件名与 inode 号关联起来。

  • 目录际上是一个特殊的文件,其中包含了多个 dentry,每个 dentry 包含一个文件名和一个指向 inode 的指针。

    • · 当你访问一个文件时,系统首先在目录中查找对应的 dentry,通过 dentry 中的 inode 号找到文件的 inode,然后通过 inode 访问文件数据。

6.lstat和stat函数

其实就是用文件名获得inode,通过inode结构体获取文件的一些属性。

stat/lstat 函数:

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

参数:

path:文件路径

buf:(传出参数)存放文件属性。

返回值:

成功:0

失败:-1 errno

获取文件大小:buf.st_size

获取文件类型:buf.st_mode

获取文件权限:buf.st_mode

符号穿透:stat会。lstat不会。

demo

cpp 复制代码
#include <sys/stat.h>
#include <pthread.h>

int main(int argc, char *argv[])
{
    struct stat sb;
    int ret = stat(argv[1], &sb);
    if (ret == -1) {
        perror("stat error");
        exit(1);
    }

    if (S_ISREG(sb.st_mode)) {
        printf("It's a regular\n");
    } else if (S_ISDIR(sb.st_mode)) {
        printf("It's a dir\n");
    } else if (S_ISFIFO(sb.st_mode)) {
        printf("It's a pipe\n");
    } else if (S_ISLNK(sb.st_mode)) {
        printf("it's a sym link\n");
    }

    return 0;
}

文件权限位

7.link和unlink函数

link函数

硬链接和软连接的区别主要是在,硬链接是不同的dentry但还是相同的inode,相当于给文件取了一个别名(我的理解)而软连接是新创建了一个文件用于链接可以类比windows中的快捷方式。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int main(int argc, char *argv[])
{
    link(argv[1], argv[2]);
    unlink(argv[1]);
    return 0;
}

unlink函数(调用unlink后并没有直接删除,而是系统择机删除)

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(void)
{
    int fd;
    char *p = "test of unlink\n";
    char *p2 = "after write something.\n";

    fd = open("temp.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd < 0){
        perror("open temp error");
        exit(1);
    }

    int ret = write(fd, p, strlen(p));
    if (ret == -1) {
        perror("----write error");
    }

    printf("hi! I'm printf\n");
    ret = write(fd, p2, strlen(p2));
    if (ret == -1) {
        perror("----write error");
    }

    printf("Enter anykey continue\n");
    getchar();
    close(fd);

    int unlink_ret = unlink("temp.txt");
    if(unlink_ret < 0){
        perror("unlink error");
        exit(1);
    }
    return 0;
}

7.opendir和closedir以及readdir()

7.1文件夹的权限概念

7.2opendir()

根据传入的目录名打开一个目录(库函数)

DIR *opendir(const char *name); 成功返回指向该目录结构体指针,失败返回 NULL

参数支持相对路径、绝对路径两种方式:DIR是目录结构体,目录里有许多目录项(dentry)

'例如:打开当前目录:① getcwd(), opendir() ② opendir(".");

7.3closedir()

关闭打开的目录。

int closedir(DIR *dirp); 成功:0;失败:-1 设置 errno 为相应值。

7.4readdir函数

struct dirent *readdir(DIR *dirp);

参数

  • dirp:一个指向 DIR 结构的指针,该结构由 opendir 函数返回,表示一个已经打开的目录。

返回值

  • 成功时,readdir 返回一个指向 struct dirent 结构的指针,该结构包含了目录中下一个条目的信息。

  • 如果到达了目录流的末尾,返回 NULL

  • 如果发生错误,也返回 NULL 并设置 errno 以指示错误类型。

返回值struct dirent内容

cpp 复制代码
#include <dirent.h>

struct dirent {
    long d_ino;           // 文件的 inode 编号
    off_t d_off;          // 文件在目录中的偏移量
    unsigned short d_reclen; // 结构体的长度
    unsigned char d_type;     // 文件类型
    char d_name[256];       // 文件或目录的名称
};

demo(实现递归遍历目录)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
void isFile(char *path);
void isDir(char *path){
    DIR *dir = opendir(path);
    char buf[256];
    if(dir == NULL){
        perror("opendir error");
        exit(1);
    }
    struct dirent *entry;
        while((entry = readdir(dir)) != NULL){ {
            //递归调用需要拼接路径 默认目录里都带有.和..代表当前文件夹和上一级文件夹
            if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0){
                continue; // 跳过当前目录和上级目录
            }
            //path是目录,d_name是文件名
            sprintf(buf, "%s/%s", path, entry->d_name);
            isFile(buf);
        }
    }
}
void isFile(char *path){
    struct stat buf;
    int ret = stat(path,&buf);
    if(ret==-1){
        perror("stat error");
        exit(1);
    }
    if(S_ISDIR(buf.st_mode)){
        //目录处理逻辑
        isDir(path);
    }
    //文件处理逻辑
    printf("%s \t %ld\n",path,buf.st_size);
}
int main(int argc, char *argv[]) {
    if(argc==1){
        isFile(".");
    }else{
        //遍历多参数
        for(int i=1;i<argc;i++){
            isFile(argv[i]);
        }
    }
    return 0;
}

8.dup和dup2重定向函数

这里的重定向其实就是将fd(文件描述符)重定向掉其他文件,比如fd=1 本来指向hello.c文件,我们可以通过重定向,将fd=1 指向world.c文件。 这样可以实现相同的fd来操作不同的文件。

8.dup和dup2重定向函数

dup 函数

dup 函数用于复制一个现有的文件描述符,并返回一个新的文件描述符,该描述符与原始文件描述符引用相同的文件或资源。

函数原型

复制代码
int dup(int oldfd);

参数

  • oldfd:要复制的原始文件描述符。

返回值

  • 成功时,返回一个新的文件描述符。

  • 失败时,返回 -1,并设置 errno 以指示错误。

dup2函数

dup2 函数不仅复制文件描述符,还可以指定新文件描述符的值。如果指定的新文件描述符已经打开,它会被关闭并替换为新的文件描述符。

复制代码
int dup2(int oldfd, int newfd);

参数

  • oldfd:要复制的原始文件描述符。

  • newfd:指定的新文件描述符的值。

返回值

  • 成功时,返回新的文件描述符(通常与 newfd 相同)。

  • 失败时,返回 -1,并设置 errno 以指示错误。

demo

cpp 复制代码
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

int main(int argc, char *argv[])
{
    int fd1 = open(argv[1], O_RDWR);        // 012 --- 3
    int fd2 = open(argv[2], O_RDWR);        // 012 --- 3
    //如果无异常fd2和fdret相同
    int fdret = dup2(fd1, fd2);
    printf("fdret = %d\n", fdret);
    //向fd2写入数据,但其实此时已经是像fd1写入数据了
    int ret = write(fd2, "1234567", 7);
    printf("ret = %d\n", ret);
    //此时可以用新的操作旧的了。这里新的是标准输入流,如果对标准输入流进行读操作,实际上是对fd1进行读操作
    dup2(fd1, STDOUT_FILENO);
    printf("-----------------------------886\n");
    return 0;
}