Linux系统编程-文件系统

目录

什么是Linux文件系统

文件系统的职责

存储介质抽象

inode:文件系统的核心

文件分配策略

目录结构

文件系统布局

日志和恢复机制

目录权限

粘滞位(t位):

硬链接和符号链接

硬链接的特点:

创建硬链接:

符号链接的特点:

创建符号链接:

文件属性

1.stat

2.fstat

3.lstat

4.chmod&fchmod

目录操作

目录的创建和销毁

更改当前工作路径

目录流操作

日期和时间

1.time

2.gmtime

3.localtime

4.strftime

/etc目录下的文件

/etc/passwd

etc/group

/etc/shadow

GLOB函数

利用glob实现du功能

利用glob实现shell的ls功能


什么是Linux文件系统

在操作系统的世界中,文件系统扮演着至关重要的角色,它负责管理磁盘上的数据,提供数据持久化和访问的机制。Linux文件系统,以其强大的灵活性和高性能,成为了许多系统管理员和开发者的首选。本文将从底层角度,深入探讨Linux文件系统的内部工作原理。

文件系统的职责

首先,让我们明确文件系统的基本职责:

  1. 数据持久化:将数据从易失性的内存转移到非易失性的存储介质上。

  2. 数据访问:提供一种机制,允许用户和应用程序读取和写入数据。

  3. 数据组织:以一种逻辑和层次化的方式组织数据,通常以目录和文件的形式呈现。

存储介质抽象

Linux文件系统将物理存储介质抽象为逻辑存储单元,这个过程涉及到几个关键概念:

块设备:文件系统通过块设备接口与磁盘交互,块设备提供了一个统一的接口来处理不同类型存储设备的I/O操作。

缓冲区:为了提高I/O效率,文件系统使用缓冲区暂存即将写入磁盘或从磁盘读取的数据。

inode:文件系统的核心

每个文件或目录在Linux文件系统中都有一个对应的inode(索引节点)。inode包含了文件的元数据,而不是文件的数据本身。这包括:

文件大小

权限(所有者、组、其他)

时间戳(创建、访问、修改时间)

数据块索引

文件分配策略

文件系统需要决定如何将文件数据分配到磁盘上的数据块。这涉及到多种文件分配策略:

连续分配:在早期的文件系统中使用,将文件数据存储在连续的磁盘块中。

链式分配:每个数据块包含指向下一个数据块的指针。

索引分配:使用一个索引块来记录数据块的位置。

现代文件系统,如Ext4和XFS,采用更为复杂的分配策略,以提高性能和灵活性。

目录结构

Linux文件系统的目录结构由目录项(Dentry)实现,每个目录项包含:

文件或目录的名称

指向对应inode的指针

这种结构使得文件和目录的查找变得非常高效。

文件系统布局

在磁盘上,文件系统的布局通常包括:

超级块:包含文件系统的全局信息,如块大小、inode数量等。

inode表:存储所有inode的区域。

数据块区:存储文件数据的区域。

日志和恢复机制

现代文件系统使用日志来记录文件系统的操作,这有助于在系统崩溃后恢复文件系统的状态。例如,Ext4文件系统使用journaling来保证数据的一致性。


目录权限

在Linux中,目录权限通常以10个字符的字符串表示,例如-drwxrwxrwt。这个字符串从左到右的含义如下:

第一个字符:

d:目录(directory)。

-:普通文件(regular file)。

l:符号链接(symbolic link)。

b:块设备文件(block device)。

c:字符设备文件(character device)。

p:管道文件(named pipe,FIFO)。

s:套接字文件(socket)
接下来的三组字符(每组三个字符)

第一组表示所有者的权限(owner)。

第二组表示与所有者同一组的用户的权限(group)。

第三组表示其他所有用户的权限(others)。

其中r(4)表示可读,w(2)可写,x(1)可执,s表示权限切换user
特殊权限位:

如果在第三组权限的末尾有一个 t,则表示设置了++粘滞位++。

粘滞位(t位):

在Linux系统中通常被称为 "t" 位,是一种特殊的文件系统权限位,它对目录的行为有特定的影响。当对目录设置了粘滞位后,只有该目录的拥有者和文件的所有者才能删除或者重命名目录中的文件。

粘滞位的主要作用:

1.防止删除和重命名 :如果一个目录设置了粘滞位,那么只有文件的所有者和目录的所有者可以删除或者重命名该目录下的文件。这可以防止普通用户删除或重命名其他用户在该目录下的文件。

2.保护公共目录:粘滞位通常用于公共目录,如 /tmp,以确保用户可以创建临时文件,但不能删除或重命名其他用户的临时文件。

如何设置粘滞位:

在Linux系统中,可以通过 chmod 命令来设置粘滞位。设置粘滞位的命令格式如下:

bash 复制代码
chmod +t <directory>

硬链接和符号链接

**硬链接(Hard Link)**是一种文件链接方式,它直接指向文件的数据所在的位置,即文件系统中的inode(索引节点)。硬链接不是文件的副本,它与原始文件共享相同的inode和数据块。这意味着硬链接和原始文件是完全相同的,对硬链接的修改实际上是对原始文件的修改,反之亦然。硬链接在文件系统管理、备份和数据恢复等场景中非常有用,因为它们提供了对原始数据的直接访问,而不需要复制数据。然而,由于硬链接的特性,使用时也需要谨慎,以避免意外覆盖或删除重要文件。可以使用stat查询Links数,即inode硬链接数量。

硬链接的特点:

1.共享inode :硬链接共享相同的inode和数据块,因此它们具有相同的inode号。

2.文件名无关 :硬链接可以位于不同的目录中,与文件名无关。硬链接的创建不会影响文件的目录结构。

3.不可跨文件系统 :硬链接不能跨越不同的文件系统创建。硬链接必须位于与原始文件相同的文件系统中。

4.删除行为 :只有当指向同一个inode的所有硬链接都被删除后,文件的数据才会被系统释放。删除硬链接不会删除原始文件或其它硬链接。

5.不适用于目录和分区:硬链接通常不用于目录,因为目录的硬链接可能导致文件系统中的循环,从而引发问题。

创建硬链接:

在Linux中,可以使用 ln 命令创建硬链接,命令格式如下:

bash 复制代码
ln existing_file new_link

这里的 existing_file 是已存在的文件,new_link 是要创建的硬链接的名称。

符号链接(Symbolic Link,也称为Symlink) 是一种特殊的文件类型,它包含了指向另一个文件或目录的路径。符号链接可以视为一个快捷方式,它允许用户通过链接访问目标文件或目录,就像直接访问原始文件或目录一样。

Blocks为0不占内存空间

符号链接的特点:

1.包含路径 :符号链接是一个单独的文件,包含了指向另一个文件或目录的路径。

2.跨文件系统 :符号链接可以跨越不同的文件系统,这与硬链接不同。

3.目录和文件 :符号链接可以指向文件或目录。

4.删除独立性 :删除符号链接不会影响它所指向的原始文件或目录。

5.更新和移动 :如果原始文件或目录被移动或重命名,符号链接将变为死链接(dangling symlink),即它指向的路径不再有效。

6.权限继承:符号链接的权限与原始文件或目录的权限无关,但访问符号链接时的权限检查会应用到目标文件或目录。

创建符号链接:

在Linux中,可以使用 ln 命令的 -s 选项创建符号链接,命令格式如下:

bash 复制代码
 ln -s target_path link_name

这里的 target_path 是要链接的目标文件或目录的路径,link_name 是符号链接的名称。


文件属性

1.stat

cpp 复制代码
int stat(const char *pathname, struct stat *statbuf);

通过文件路径获取属性信息填入 struct stat中,成功返回0,失败返回-1

2.fstat

cpp 复制代码
int fstat(int fd, struct stat *statbuf);

通过文件描述符获取属性信息填入 struct stat中,成功返回0,失败返回-1

3.lstat

cpp 复制代码
int lstat(const char *pathname, struct stat *statbuf);


struct stat {
    dev_t st_dev;         /* 包含文件的设备的ID */
    ino_t st_ino;         /* inode(索引节点)编号 */
    mode_t st_mode;       /* 16位的位图,表示文件类型,文件访问权限,特殊权限位 */
    nlink_t st_nlink;     /* 硬链接的数量 */
    uid_t st_uid;         /* 所有者的用户名ID */
    gid_t st_gid;         /* 所有者的组ID */
    dev_t st_rdev;        /* 特殊文件的设备ID */
    off_t st_size;        /* 总大小,单位为字节 */
    blksize_t st_blksize; /* 文件系统I/O的块大小 */
    blkcnt_t st_blocks;  /* 分配的512B块的数量 */

    /* 自 Linux 2.6 起,内核支持以下时间戳字段的纳秒级精度。
       有关 Linux 2.6 之前的详细信息,请参阅注释。 */

    struct timespec st_atim;  /* 最后访问时间 */
    struct timespec st_mtim;  /* 最后修改时间 */
    struct timespec st_ctim;  /* 最后状态改变时间 */

    #define st_atime st_atim.tv_sec      /* 向后兼容 */
    #define st_mtime st_mtim.tv_sec
    #define st_ctime st_ctim.tv_sec
};

面对符号链接文件时获取的是符号链接文件的属性。

4.chmod&fchmod

cpp 复制代码
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);

改变权限,成功返回0,失败-1

S_ISUID (04000) 设置用户ID(在调用 execve(2) 时设置进程有效用户ID)

S_ISGID (02000) 设置组ID(在调用 execve(2) 时设置进程有效组ID;强制性锁定,如 fcntl(2) 中所述;从父目录获取新文件的组,如 chown(2) 和 mkdir(2) 中所述)

S_ISVTX (01000) 粘滞位(限制删除标志,如 unlink(2) 中所述)

S_IRUSR (00400) 所有者读取

S_IWUSR (00200) 所有者写入

S_IXUSR (00100) 所有者执行/搜索("搜索"适用于目录,意味着可以访问目录内的条目)

S_IRGRP (00040) 组内成员读取

S_IWGRP (00020) 组内成员写入

S_IXGRP (00010) 组内成员执行/搜索

S_IROTH (00004) 其他用户读取

S_IWOTH (00002) 其他用户写入

S_IXOTH (00001) 其他用户执行/搜索


目录操作

目录的创建和销毁

cpp 复制代码
int mkdir(const char *pathname, mode_t mode);

创建一个新目录。如果目录的父目录不存在,或者没有足够的权限,函数将失败。

cpp 复制代码
int rmdir(const char *pathname);

删除一个空目录。如果目录非空,或者指定路径不存在,或者没有足够的权限,函数将失败。

更改当前工作路径

cpp 复制代码
int chdir(const char *path);

改变(切换)当前工作目录到指定的路径。如果路径不存在或没有足够的权限访问该路径,函数将失败。

cpp 复制代码
int fchdir(int fd);

改变当前工作目录到由文件描述符 fd 指向的目录。

cpp 复制代码
char *getcwd(char *buf, size_t size);

获取当前工作目录的绝对路径,并将其复制到由 buf 指向的缓冲区中。如果缓冲区大小不足以存储路径,函数将失败。

目录流操作

cpp 复制代码
DIR *opendir(const char *name);
DIR *fdopendir(int fd);

打开文件夹获取DIR流指针,失败返回NULL

cpp 复制代码
struct dirent *readdir(DIR *dirp);


    struct dirent {
        ino_t d_ino;       /* inode编号 */
        off_t d_off;       /* 并非偏移量;详见下文 */
        unsigned short d_reclen; /* 此记录的长度 */
        unsigned char d_type; /* 文件类型;并非所有文件系统类型都支持 */
        char d_name[256]; /* 以空字符结尾的文件名 */
    };

返回指向struct dirent指针,dirent里存储了目录的信息,失败返回空指针


日期和时间

1.time

cpp 复制代码
time_t time(time_t *tloc);

时间戳是从 1970 年 1 月 1 日(UTC 时间)开始计算的Unix 时间戳,tloc如果提供了这个参数,函数会将当前时间的时间戳存储在这个指针指向的位置。如果这个参数是 NULL 或者没有提供,函数不会写入任何值。

2.gmtime

cpp 复制代码
struct tm *gmtime(const time_t *timep);


struct tm {
        int tm_sec;    /* Seconds (0-60) */
        int tm_min;    /* Minutes (0-59) */
        int tm_hour;   /* Hours (0-23) */
        int tm_mday;   /* Day of the month (1-31) */
        int tm_mon;    /* Month (0-11) */
        int tm_year;   /* Year - 1900 */
        int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
        int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
        int tm_isdst;  /* Daylight saving time */
};

Unix时间转为tm结构体,该结构体表示的是协调世界时UTC

3.localtime

cpp 复制代码
struct tm *localtime(const time_t *timep);

Unix时间转为tm结构体,会根据本地时区和夏令时调整来转换时间

4.strftime

cpp 复制代码
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

根据给定的格式字符串,将 tm 结构体中的时间信息格式化为可读的字符串,并将其存储在提供的字符数组 s 中

参数

s:指向字符数组的指针,用于存储格式化后的字符串。

max:s 数组的最大长度,即可以存储的最大字符数,包括空字符('\0')。

format:一个格式字符串,指定了时间的格式化方式。格式字符串中可以包含特殊的格式说明符,如 %Y 表示四位数的年份等。

tm:指向 tm 结构体的指针,包含了要格式化的时间信息。

返回值

函数返回实际写入到 s 数组中的字符数(不包括结尾的空字符)。如果由于缓冲区大小不足而无法写入整个字符串,返回值会小于 max。如果 s 是 NULL 或 max 是 0,则函数不执行格式化操作,但仍然会返回需要的缓冲区大小(不包括结尾的空字符)。

格式化说明符

%a:缩写的星期名称。

%A:完整的星期名称。

%b:缩写的月份名称。

%B:完整的月份名称。

%c:适合人类可读的日期和时间。

%d:月份中的第几天(01-31)。

%H:小时(24小时制,00-23)。

%I:小时(12小时制,01-12)。

%m:月份(01-12)。

%M:分钟(00-59)。

%S:秒(00-59)。

%Y:四位数的年份。


/etc目录下的文件

/etc/passwd

文件中的每行都包含一个用户账户的信息,字段之间用冒号(:)分隔。标准的字段包括:

-用户名:用户的登录名。

-密码:通常是一个"x"字符,表示密码存储在 /etc/shadow 文件中,或者是加密后的密码散列值。

-用户ID(UID):用户的唯一标识符,通常是数字。

-组ID(GID):用户所属主组的唯一标识符。

-主目录:用户的主目录路径。

-登录shell:用户登录时使用的 shell。

格式示例如下:

username:x:UID:GID:User Name:/home/username:/bin/bash

具体解释如下:

username:用户的登录名。

x:表示密码存储在 /etc/shadow 文件中。

UID:用户的唯一标识符。

GID:用户所属主组的唯一标识符。

User Name:用户的全名或描述。

/home/username:用户的主目录路径。

/bin/bash:用户的默认登录 shell。

cpp 复制代码
struct passwd *getpwnam(const char *name);


struct passwd {
         char   *pw_name;       /* username */
         char   *pw_passwd;     /* user password */
         uid_t   pw_uid;        /* user ID */
         gid_t   pw_gid;        /* group ID */
         char   *pw_gecos;      /* user information */
         char   *pw_dir;        /* home directory */
         char   *pw_shell;      /* shell program */
};

通过用户名获取用户信息

cpp 复制代码
struct passwd *getpwuid(uid_t uid);

通过用户uid获取用户信息

etc/group

是 Linux 和类 Unix 系统中的另一个重要文件,它用于存储用户组的信息。每个条目代表一个用户组,并且每行的字段通常由冒号(:)分隔。标准的字段包括:

-组名:用户组的名称,通常不包含空格。

-密码:用户组的加密密码,或者是一个"x"字符,表示密码存储在 /etc/gshadow 文件中。

-组ID(GID):用户组的唯一标识符,是一个数字。

-用户列表:属于该组的用户列表,可以是用户名或用户名的逗号分隔列表。

格式示例如下

groupname:password:GID:user1,user2,user3

具体解释如下

groupname:用户组的名称。

password:用户组的加密密码,或者是一个"x"字符表示密码存储在 /etc/gshadow 文件中。

GID:用户组的唯一标识符。

user1,user2,user3:属于该组的用户列表,用户之间用逗号分隔。

cpp 复制代码
struct group *getgrnam(const char *name);


struct group {
         char   *gr_name;        /* group name */
         char   *gr_passwd;      /* group password */
         gid_t   gr_gid;         /* group ID */
         char  **gr_mem;         /* NULL-terminated array of pointers
                                          to names of group members */
};

通过组名获取组信息

cpp 复制代码
struct group *getgrgid(gid_t gid);

通过组gid获取组信息

/etc/shadow

文件在类 Unix 系统中用于存储用户账户的密码信息,以增强安全性。该文件通常只能由 root 用户访问,并且包含加密后的密码数据。每行对应一个用户,字段之间用冒号(:)分隔。标准的字段包括:

-用户名:与 /etc/passwd 文件中的用户名相同。

-加密密码:用户的加密密码或一个特定的值,如 * 或 !,表示账户被锁定或密码无效。

-最后一次更改密码的日期:以从1970年1月1日(epoch)开始的天数计算。

-密码最小更改间隔:用户必须等待的最小天数,才能再次更改密码。

-密码最大有效期限:密码的最大有效天数。

-密码警告期:在密码过期前,系统会警告用户的天数。

密码到期后账户被禁用的天数:密码过期后,用户账户被禁用的天数。

-预留字段:通常为空。

格式示例如下:
username:6somehash$somehash:17000:0:99999:7:7:::

具体解释如下:

username:用户的登录名。

6somehash$somehash:加密的密码,其中 6 表示使用的是 SHA-512 加密算法。

17000:最后一次更改密码的日期(天数)。

0:密码最小更改间隔。

99999:密码最大有效期限。

7:密码警告期。

7:密码到期后账户被禁用的天数。

:::预留字段,通常为空。


GLOB函数

cpp 复制代码
int glob(const char *pattern, int flags,
                int (*errfunc) (const char *epath, int eerrno),
                glob_t *pglob);


typedef struct {
         size_t gl_pathc;    /* 到目前为止匹配的路径数量 */
         char **gl_pathv;    /* 匹配的路径名列表 */
         size_t gl_offs;     /* 在 gl_pathv 中预留的槽位数 */
} glob_t;

参数说明:

--pattern:一个指向以null结尾的字符串的指针,指定了要匹配的文件名模式(文件路径)。模式可以包含如下特殊字符:

*:匹配任意数量的字符。

?:匹配任意单个字符。

[...]:匹配括号内的任意一个字符。

--flags:用于控制glob函数行为的标志位,可以是以下值的组合:

GLOB_APPEND:如果pglob已经包含一些路径,新的路径将被追加到现有列表中,没有APPEND则是覆盖。

GLOB_DOOFFS:减少分配给pglob->gl_pathv数组的内存量,数组大小为pglob->gl_pathc + 1。

GLOB_ERR:如果发生错误并且提供了错误函数,函数将立即调用错误处理函数。

GLOB_MARK:在每个匹配的路径名末尾添加一个斜杠(/)。

GLOB_NOCHECK:如果模式没有匹配任何文件,返回空列表而不是GLOB_NOMATCH错误。

GLOB_NOSORT:不按字母顺序对匹配的路径进行排序。

--errfunc:当出现错误并且GLOB_ERR标志被设置时,将调用此错误处理函数。该函数接受两个参数:错误路径和错误号。不需要错误检查时可以设置NULL。

--pglob:指向glob_t结构的指针,该结构用于存储匹配的路径列表和相关信息。

返回值

成功时,返回 0。

当没有找到匹配的文件名时,如果设置了GLOB_NOCHECK标志,返回 0,否则返回 GLOB_NOMATCH。

发生错误时,返回非零错误代码。

cpp 复制代码
void globfree(glob_t *pglob);

销毁pglob申请空间,主要是char **gl_pathv;字符数组申请的空间


利用glob实现du功能

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <glob.h>
#include <string.h>

#define PATHSIZE 128

static path_noloop(const char *path) {
    char *pos;
    pos = strrchr(path, '/')'
    if(pos == NULL)
        exit(1);

    if(strcmp(pos+1, ".") == 0 || strcmp(pos+1, "..") == 0)
        return 0;

    return 1;
}

static int64_t K_du(const char* path){
    struct stat statres;
    //非目录
    if(lstat(path, &statres) < 0 ){
        perror("lstat()");
        exit(1);
    }
    if(!S_ISDIR(statres.st_mode))
            return statres.st_blocks/2;
    //目录
    char *nextpath[PATHSIZE];
    glob_t globres;
    int sum = statres.st_blocks;

    strncpy(nextpath, path, PATHSIZE);
    strncat(nextpath, "/*", PATHSIZE);
    glob(nextpath, 0, NULL, &globres);

    strncpy(nextpath, path, PATHSIZE);
    strncat(nextpath, "/.*", PATHSIZE);
    glob(nextpath, GLOB_APPEND, NULL, &globres);

    for(int i = 0; i < globres.gl_pathc; i++){
        if(path_noloop(globres.gl_pathv[i]))
            sum += mydu(globres.gl_pathv[i]);
    }

    return sum;
}

利用glob实现shell的ls功能

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <glob.h>
#include <sys/wait.h>

// 定义命令分隔符
#define DELIMS " \t\n"

// 定义一个结构体,用于存储 glob 函数的返回结果
struct cmd_st {
    glob_t globres;
};

// promopt 函数用于显示提示符
static void promopt(void) {
    printf("[myshell-knoci-1.2] $ ");
}

// parse 函数用于解析用户输入的命令行
static void parse(char *line, struct cmd_st *cmd) {
    char *token;  // 用于存储分割出的每个单词
    int i = 0;    // 用于控制是否追加到 glob 结构

    // 使用 strsep 函数分割字符串,直到没有更多单词
    while(1) {
        token = strsep(&line, DELIMS);
        if (token == NULL)
            break;
        if(token[0] == '\0')  // 跳过空字符串
            continue;

        // 对每个分割出的单词进行 glob 匹配,第一个单词不追加,后续追加
        glob(token, GLOB_NOCHECK | (i ? GLOB_APPEND : 0), NULL, &cmd->globres);
        i = 1;
    }
}

// 主函数
int main(int argc, char *argv[]) {
    char *linebuf = NULL;  // 用于存储 getline 读取的整行命令
    size_t linebuf_size = 0; // linebuf 的当前分配大小
    struct cmd_st cmd;      // 定义 cmd 结构用于存储 glob 结果
    // 初始化 glob 结构的指针和计数器
    cmd.globres.gl_pathv = NULL;
    cmd.globres.gl_pathc = 0;
    pid_t pid;  // 存储 fork 函数返回的子进程 ID

    // 无限循环,持续读取和执行用户命令
    while(1) {
        promopt();  // 显示提示符

        // 使用 getline 函数从标准输入读取一行
        if(getline(&linebuf, &linebuf_size, stdin) < 0)
            break;

        // 解析命令行
        parse(linebuf, &cmd);

        // 假设没有内部命令,所有命令都是外部命令
        pid = fork();  // 创建子进程
        if(pid < 0) {
            perror("fork");  // 显示 fork 出错信息
            exit(1);
        }

        // 在子进程中执行命令
        if(pid == 0) {
            // 子进程中调用 execvp 执行命令
            if(cmd.globres.gl_pathc > 0) {
                execvp(cmd.globres.gl_pathv[0], cmd.globres.gl_pathv);
            }
            perror("execvp");  // 如果 execvp 失败,显示错误信息
            exit(1);  // 退出子进程
        } else {
            // 父进程等待子进程结束
            int status;
            waitpid(pid, &status, 0);
        }
    }

    // 清理资源
    globfree(&cmd.globres);  // 释放 glob 分配的内存
    free(linebuf);  // 释放 getline 分配的内存
    exit(0);  // 正常退出程序
}
相关推荐
world=hello9 分钟前
关于科研中使用linux服务器的集锦
linux·服务器
枫零NET11 分钟前
学习思考:一日三问(学习篇)之匹配VLAN
网络·学习·交换机
沐泽Mu35 分钟前
嵌入式学习-QT-Day07
c++·qt·学习·命令模式
沐泽Mu36 分钟前
嵌入式学习-QT-Day09
开发语言·qt·学习
小猿_0041 分钟前
C语言实现顺序表详解
c语言·开发语言
炸毛的飞鼠41 分钟前
汇编语言学习
笔记·学习
soragui1 小时前
【ChatGPT】OpenAI 如何使用流模式进行回答
linux·运维·游戏
egekm_sefg1 小时前
webrtc学习----前端推流拉流,局域网socket版,一对多
前端·学习·webrtc
Dola_Pan1 小时前
C语言:随机读写文件、实现文件复制功能
c语言·开发语言