标签 Linux 下的文章

前言

此篇文章为学习 Linux系统编程02:文件系统 部分的笔记

1. 文件存储

1.1 inode

inode 是 Linux 和 Unix 操作系统中的一个重要概念,它是文件系统中的一个 数据结构,用于存储文件的元数据。每个文件和目录都有一个对应的 inode 来描述其属性和位置信息。

root@freecho:/opt/C/gcc/code# stat hello.c
  File: hello.c
  Size: 373             Blocks: 8          IO Block: 4096   regular file
Device: b301h/45825d    Inode: 1314593     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2023-07-26 21:29:11.655510006 +0800
Modify: 2023-07-26 21:29:11.655510006 +0800
Change: 2023-07-26 21:29:11.655510006 +0800
Birth: 2023-07-26 21:29:11.655510006 +0800

inode 结构包含了以下信息:不包含文件名

  1. 文件类型:标识文件是普通文件、目录、符号链接等类型。
  2. 文件权限:文件的读、写、执行权限。
  3. 文件所有者和所属组:标识文件的所有者和所属的用户组。
  4. 文件大小:文件的大小,以字节为单位。
  5. 文件时间戳:记录文件的创建时间、修改时间和访问时间等。
  6. 文件链接数:记录文件的硬链接数目。
  7. 数据块指针:指向文件存储数据的数据块。

当系统中创建一个新文件时,会为该文件分配一个唯一的 inode,然后将文件的实际数据存储在数据块中,并将 inode 中的数据块指针指向这些数据块。在文件被访问或修改时,通过 inode 可以快速定位文件的数据块,而不需要遍历整个文件系统。

通过 ls -i 命令可以查看文件的 inode 号码。每个文件和目录在同一文件系统中具有唯一的 inode 号码。

inode 在文件系统的性能和管理中起着关键作用,它使得文件系统能够高效地管理文件和目录,并支持硬链接的使用。

1.2 dentry

dentry , (即 directory entry 目录项)是 Linux 文件系统中的一个重要概念,用于管理文件系统中的目录。dentryinode 相关联,共同组成了 Linux 文件系统中的目录项。其本质依然是结构体,重要成员变量有两个 {文件名,inode,...},而文件内容(data)保存在磁盘盘块中

每个目录都包含一个或多个 dentry,每个 dentry 表示一个目录中的文件或子目录。dentry 记录了文件或目录的名称、文件类型和对应的 inode 号码等信息。当用户访问文件时,Linux 文件系统会通过 dentry 来快速定位文件的 inode,从而访问文件的实际数据。

在 Linux 文件系统中,dentry 会被缓存在内存中,以提高文件系统的性能。当用户访问文件时,系统首先会查找该文件对应的 dentry 是否已经缓存,如果已经缓存,则直接从 dentry 中获取 inode 信息,避免了不必要的磁盘访问。如果文件对应的 dentry 不在缓存中,系统会通过目录索引进行查找,并将找到的 dentry 缓存起来,以便下次快速访问。

dentry 与目录层次结构一起形成了文件系统的层次结构,通过 dentry 可以在文件系统中快速定位文件和目录,提高了文件系统的访问效率和性能。同时,dentry 的缓存机制也减少了不必要的磁盘访问,提高了整个文件系统的效率。

1.3 文件系统

文件系统是一组规则,规定对文件的存储及读取的一般方法。文件系统在磁盘格式化过程中指定。

以下为常见文件系统:

  1. FAT32(File Allocation Table 32):FAT32是一种较旧的文件系统,广泛应用于可移动介质(如USB闪存驱动器、SD卡等)。它是Windows系统和其他操作系统的通用文件系统。
  2. NTFS(New Technology File System):NTFS是Windows操作系统中使用的主要文件系统。它支持大文件和文件系统,并提供更高级的权限控制和数据安全性。
  3. exFAT(Extended File Allocation Table):exFAT是FAT32文件系统的改进版本,特别设计用于支持更大的文件和分区。它通常在移动存储设备和外部驱动器中使用。
  4. ext2(Second Extended File System):ext2是Linux系统早期的文件系统,不具备日志功能。虽然现在很少使用,但仍然是一些老旧系统的选择。
  5. ext3(Third Extended File System):ext3是ext2文件系统的改进版本,具有日志记录功能,可提供更好的数据完整性和恢复能力。
  6. ext4(Fourth Extended File System):ext4是Linux系统中目前最常用的文件系统,它是ext3文件系统的进一步改进,提供更高的性能和可靠性。ext4支持更大的文件和文件系统,并具备更高级的特性。

1.4 硬链接、软连接

硬链接软链接 (又称软连接)是 Linux 文件系统中两种不同类型的链接方式,用于在文件系统中创建文件或目录之间的关联。

  1. 硬链接(Hard Link):


    • 硬链接是目录项(dentry)中指向相同 inode 号的不同目录项。
    • 通过硬链接,多个文件名可以指向同一个数据块,实际上是同一个文件的不同访问入口。
    • 硬链接创建后,可以像普通文件一样操作,读写内容,删除等,但是不能对目录进行硬链接。
    • 硬链接不能跨文件系统创建,即硬链接必须位于同一个文件系统。
  2. 软链接(Symbolic Link / Soft Link):


    • 软链接是一个特殊的文件,它包含了指向另一个文件或目录的路径名。
    • 软链接类似于 Windows 系统的快捷方式,它只是一个指向目标的快捷方式而已。
    • 软链接可以跨文件系统创建,因为它只保存了目标文件或目录的路径名。
    • 删除软链接并不会影响目标文件或目录,但如果目标文件或目录被删除,软链接将变为"断链"。

对比:

  • 硬链接是多个目录项指向同一个 inode,它们是文件系统中同一个文件的不同名字,文件大小和权限都是相同的。
  • 软链接是一个特殊的文件,它保存了指向目标文件或目录的路径名,它是目标文件或目录的"快捷方式",不占用实际数据块。

注意事项:

  • 删除硬链接或软链接并不会删除目标文件本身。
  • 硬链接不能跨文件系统创建,而软链接可以。

示例:

$ echo "Hello, hard link!" > original.txt
$ ln original.txt hard_link.txt    # 创建硬链接
$ ln -s original.txt soft_link.txt # 创建软链接

$ ls -l
-rw-r--r--  2 user user 18 May 18 2023 hard_link.txt
lrwxrwxrwx  1 user user 13 May 18 2023 soft_link.txt -> original.txt

$ cat hard_link.txt  # 输出:"Hello, hard link!"
$ cat soft_link.txt  # 输出:"Hello, hard link!"

$ rm original.txt   # 删除原始文件
$ cat hard_link.txt  # 输出:"Hello, hard link!",硬链接仍然存在
$ cat soft_link.txt  # 输出:"cat: soft_link.txt: No such file or directory",软链接断链

 

2. 文件操作

2.1 stat、lstat 函数

概念

statlstat函数都用于获取文件或目录的信息,但在处理符号链接时有所不同。

  1. stat函数:


    • 函数原型:int stat(const char *path, struct stat *buf);
    • 描述:stat函数通过指定的文件路径获取文件信息,并将结果存储在struct stat类型的结构体buf中。如果path是一个符号链接,stat函数将会获取符号链接指向的文件的信息。
    • 返回值:成功时返回0,失败时返回-1。
  2. lstat函数:


    • 函数原型:int lstat(const char *path, struct stat *buf);
    • 描述:lstat函数与stat函数类似,也用于获取文件信息。不同之处在于,lstat函数不会跟随符号链接,而是获取符号链接本身的信息,而不是它所指向的文件的信息。
    • 返回值:成功时返回0,失败时返回-1。

这两个函数对于获取文件的权限、大小、时间戳等信息非常有用,而在处理符号链接时,使用lstat函数可以避免不必要的问题。在编写程序时,需要根据具体需求选择使用stat函数还是lstat函数。

buf.st_size  // 获取文件大小
buf.st_mode  // 获取文件类型
buf.st_mode  // 获取文件权限
符号穿透:stat 会  lstat 不会

代码

stat.c :查看文件大小

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

int main(int argc, char *argv[])
{
    struct stat sbuf;

    int ret = stat(argv[1], &sbuf);
    if (ret == -1) {
        perror("stat error");
        exit(1);
    }

    printf("file size: %ld\n", sbuf.st_size);

    return 0;
}

lstat.c : 查看文件属性

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

int main(int argc, char *argv[])
{
    struct stat sbuf;

    int ret = lstat(argv[1], &sbuf);
    if (ret == -1) {
        perror("stat error");
        exit(1);
    }

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

    return 0;
}

2.2 link、unlink 函数

概念

link函数和unlink函数用于创建硬链接和删除文件链接(硬链接或符号链接)。

  1. link函数: link函数用于创建硬链接。硬链接是指在文件系统中创建一个新的链接指向同一个文件,这个新链接和原文件具有相同的inode号和数据块,但是在目录中显示为一个新的文件名。它的原型为:

    int link(const char *oldpath, const char *newpath);
    

    参数说明:


    • oldpath:源文件路径名,即要创建硬链接的文件。
    • newpath:目标文件路径名,即新创建的硬链接的文件名。

    返回值:


    • 如果成功创建硬链接,返回0。
    • 如果出现错误,返回-1,并设置errno来指示错误类型。
  2. unlink函数: unlink函数用于删除一个文件链接。如果删除的是硬链接,只会删除该链接,而不会删除原文件;如果删除的是符号链接,会删除链接指向的原文件。它的原型为:

    cint unlink(const char *pathname);
    

    参数说明:


    • pathname:要删除的文件路径名,可以是硬链接或符号链接。

    返回值:


    • 如果成功删除文件链接,返回0。
    • 如果出现错误,返回-1,并设置errno来指示错误类型。

这两个函数在Linux系统编程中经常用于文件链接的创建和删除操作。需要注意的是,link函数只能用于同一个文件系统内的文件,而不能跨文件系统创建硬链接。对于跨文件系统的文件链接,可以使用符号链接(符号链接是指在文件系统中创建一个新的文件,它指向另一个文件的路径)来实现。

思考,为什么目录项要游离于 inode 之外,画蛇添足般的将文件名单独存储呢?这样 的存储方式有什么样的好处呢? 其目的是为了实现文件共享。

Linux 允许多个目录项共享一个 inode,即共享盘块(data)。 不同文件名,在人类眼中将它理解成两个文件,但是在内核眼里是同一个文件

link 函数,可以为已经存在的文件创建目录项(硬链接)。unlink 函数则是删除一个文件的目录项

mv 命令即是修改了目录项,而并不修改文件本身。

代码

注意 Linux 下删除文件的机制:不断将 st_nlink -1,直至减到 0 为止。无目录项对应的 文件,将会被操作系统择机释放。(具体时间由系统内部调度算法决定) 因此,我们删除文件,从某种意义上说,只是让文件具备了被释放的条件。

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

mymv.c :编程实现 mv 命令的改名操作

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

    unlink(argv[1]);

    return 0;
}

unlink.c:通过观察临时文件 temp.txt 存在情况,了解unlink函数以及删除文件机制

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int fd;
    int ret;
    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);
    }

    ret = unlink("temp.txt"); // // 出现段错误时,temp.txt依然销毁
    if (ret < 0) {
        perror("unlink error");
        exit(1);
    }

    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");
    }

    p[3] = 'H';  // 发送段错误

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

    close(fd);

    /*
    ret = unlink("temp.txt"); // 出现段错误时,temp.txt无法销毁
    if (ret < 0) {
        perror("unlink error");
        exit(1);
    }
    */

    return 0;
}

2.3 隐式回收

当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统的这一特性称之为隐式回收系统资源。

写程序时一定不能忘记关闭文件

2.4 其他函数

readlink 函数

在Linux中,readlink是一个命令行工具和系统调用,用于读取符号链接(Symbolic Link)所指向的目标路径。

  1. 命令行工具: readlink命令用于查看符号链接的目标路径。使用方法如下:

    readlink [OPTIONS] LINK_PATH
    

    其中,LINK_PATH是符号链接的路径。readlink会输出该符号链接所指向的目标路径。

  2. 系统调用readlink也是一个系统调用,用于在C/C++程序中访问符号链接的目标路径。

    函数原型:

    #include <unistd.h>
    ssize_t readlink(const char *path, char *buf, size_t bufsiz);
    

    参数说明:


    • path:符号链接的路径。
    • buf:用于存储目标路径的缓冲区。
  • bufsiz:缓冲区的大小,应该足够大以容纳目标路径的字符。

返回值:

  • 成功时,返回读取的目标路径的长度(不包括终止空字符),如果目标路径长度大于bufsiz,则返回-1
  • 失败时,返回-1,并设置errno来指示错误类型。

rename 函数

rename函数是一个C标准库函数,用于对文件或目录进行重命名。它在 <stdio.h> 头文件中声明,并且是一个较为简单的文件操作函数。

函数原型:

#include <stdio.h>
int rename(const char *old_path, const char *new_path);

参数说明:

  • old_path:旧的文件名或目录名。
  • new_path:新的文件名或目录名。

返回值:

  • 如果重命名成功,则返回0。
  • 如果重命名失败,则返回-1,并设置errno来指示错误类型。

 

3. 目录操作

3.1 getcwd、chdir 函数

getcwd 函数

获取进程当前工作目录 (卷 3,标库函数)

char *getcwd(char *buf, size_t size); 

成功:buf 中保存当前进程工作目录位置

失败: NULL

chdir 函数

改变当前进程的工作目录

 int chdir(const char *path); 

成功:0

失败:-1 设置 errno 为相应值

3.2 文件、目录权限

注意:目录文件也是“文件”。其文件内容是该目录下所有子文件的目录项 dentry。 可以尝试用 vim 打开一个目录。

 rwx
文件文件的内容可以被查看内容可以被修改可以运行产生一个进程
 cat、more、less…vi、> …./文件名
目录目录可以被浏览创建、删除、修改文件可以被打开、进入
 ls、tree…mv、touch、mkdir...cd

目录设置黏住位:若有 w 权限,创建不变,删除、修改只能由 root、目录所有者、文件所 有者操作。

3.3 目录函数

opendir 函数

返回 :根据传入的目录名打开一个目录 (库函数) DIR * 类似于 FILE *

DIR *opendir(const char *name); 

返回 :成功返回指向该目录结构体指针,失败返回 NULL

参数支持相对路径、绝对路径两种方式

例如:打开当前目录:

  1. getcwd() , opendir()
  2. opendir(".");

closedir 函数

作用:关闭打开的目录

int closedir(DIR *dirp); 

返回 :成功:0; 失败:-1 设置 errno 为相应值

readdir 函数

作用:读取目录 (库函数)

struct dirent *readdir(DIR *dirp); 

返回 :成功返回目录项结构体指针;失败返回NULL设置errno 为相应值

需注意返回值,读取数据结束时也返回 NULL 值,所以应借助 errno 进一步加以区分。

struct 结构体

 struct dirent { 
     ino_t d_ino;            // inode 编号 
     off_t d_off;  
     unsigned short d_reclen; // 文件名有效长度
     unsigned char d_type;   // 类型(vim 打开看到的类似@*/等)
	 char d_name[256];        // 文件名
 };

代码

myls.c:通过以上函数实现 ls 命令

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>

int main(int argc, char *argv[])
{
    DIR *dp;
    struct dirent *sdp;

    dp = opendir(argv[1]);
    if (dp == NULL) {
        perror("open dir error");
        exit(1);
    }

    while ((sdp = readdir(dp)) != NULL) {
        if (strcmp(sdp->d_name, ".") == 0)
            continue;
        if (strcmp(sdp->d_name, "..") == 0)
            continue;
        printf("%s\t", sdp->d_name);
    }

    printf("\n");

    closedir(dp);

    return 0;
}

 

4. 递归遍历目录

查询指定目录,递归列出目录中文件,同时显示文件大小

4.1 思路

  1. 判断命令行参数,获取用户要查询的目录名 argv[1]

    argvc == 1 ---> ./

  2. 判断用户指定的是否是目录。不是则打印文件名

    stat S_ISDIR() ---> 封装函数 isFile

  3. 读目录:

    opendir()   
    while (readdir()) {
        普通文件:直接打印
        目录:
            拼接目录访问绝对路径   sprintf(path, "%s/%s", dir, d_name)
            递归调用自己
    } 
    closedir()
    

4.2 代码

ls-R.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>

void isFile(char *name);

// 打开目录,读取目录,处理目录
void read_dir(char *dir, void (*func)(char *))
{
    char path[256];
    DIR *dp;
    struct dirent *sdp;

    dp = opendir(dir);
    if (dp == NULL) {
        perror("opendir error");
        return;
    }

    // 读取目录项
    while ((sdp = readdir(dp)) != NULL) {
        if (strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) {
            continue;
        }
        // 目录项本身不可访问,拼接 目录/目录项
        sprintf(path, "%s/%s", dir, sdp->d_name);
        // 判断文件类型,目录递归进入,文件显示名字、大小
        //isFile(path);
        func(path);
    }

    closedir(dp);

    return;
}

void isFile(char *name)
{
    int ret = 0;
    struct stat sbuf;

    // 获取文件属性,判断文件类型
    ret = stat(name, &sbuf);
    if (ret == -1) {
        perror("stat error");
        return;
    }

    // 目录文件,进入目录函数
    if (S_ISDIR(sbuf.st_mode)) {
        read_dir(name, isFile);
    }
    // 普通文件,显示文件名、大小
    printf("%10s\t\t%ld\n", name, sbuf.st_size);

    return;
}


int main(int argc, char *argv[])
{
    // 判断命令行参数
    if (argc == 1) {
        isFile(".");
    } else {
        isFile(argv[1]);
    }

    return 0;
}

5. 重定向

dupdup2 是 Linux 系统中用于复制文件描述符的函数,它们都是 C 语言的系统调用函数。它们的作用是创建一个新的文件描述符,该文件描述符是现有文件描述符的副本,指向同一个文件。

5.1 dup 函数

int dup(int oldfd);

dup 函数会复制参数 oldfd 所指向的文件描述符,并返回一个新的文件描述符,该新的文件描述符是系统中当前可用的最小的未使用的文件描述符。

如果复制成功,则返回新的文件描述符;如果复制失败,则返回 -1。

代码

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    // 复制文件描述符
    int new_fd = dup(fd);

    printf("Original file descriptor: %d\n", fd);
    printf("New file descriptor: %d\n", new_fd);

    close(fd); // 注意:关闭原文件描述符不会影响新的文件描述符

    // 使用新的文件描述符读取文件内容
    char buffer[10];
    ssize_t bytes_read = read(new_fd, buffer, sizeof(buffer) - 1);
    buffer[bytes_read] = '\0';
    printf("Content: %s\n", buffer);

    close(new_fd);
    return 0;
}

5.2 dup2 函数

int dup2(int oldfd, int newfd);

dup2 函数与 dup 函数类似,但是它可以指定新的文件描述符的数值。如果 newfd 已经是一个打开的文件描述符,那么 dup2 将首先关闭 newfd,然后将 oldfd 复制到 newfd,确保 newfdoldfd 指向相同的文件。

成功:返回一个新文件描述符; 如果 oldfd 有效,则返回的文件描述符与 oldfd 指向同一文件。

失败:如果 oldfd 无效,调用失败,关闭 newfd。返回-1,同时设置 errno 为相应值

代码

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    // 复制文件描述符到新的文件描述符 100
    int new_fd = dup2(fd, 100);

    printf("Original file descriptor: %d\n", fd);
    printf("New file descriptor: %d\n", new_fd);

    close(fd); // 注意:关闭原文件描述符不会影响新的文件描述符

    // 使用新的文件描述符读取文件内容
    char buffer[100];
    ssize_t bytes_read = read(new_fd, buffer, sizeof(buffer) - 1);
    buffer[bytes_read] = '\0';
    printf("Content: %s\n", buffer);

    close(new_fd);
    return 0;
}

5.3 小结

  • dup 复制文件描述符,返回一个新的文件描述符,值为系统中当前可用的最小未使用的文件描述符。
  • dup2 复制文件描述符到指定的新文件描述符,如果新文件描述符已经打开,则先关闭新文件描述符再复制。
  • 这两个函数在多线程环境下可能会存在竞态条件,使用时需要注意线程安全性。

记忆方法两种:

  1. 文件描述符的本质角度理解记忆。
  2. 从函数原型及使用角度,反向记忆。

练习:借助 dup 函数编写 mycat 程序,实现 cat file1 > file2 命令相似功能

mycat.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd1, fd2;
    int fdret, ret;

    fd1 = open(argv[1], O_RDWR);
    fd2 = open(argv[2], O_RDWR);

    fdret = dup2(fd1, fd2);  // 返回 新文件描述符fd2
    printf("fdret = %d\n", fdret);

    ret = write(fd2, "1234567", 7); // 写入 fd1 指向的文件
    printf("ret = %d\n", ret);

    dup2(fd1,STDOUT_FILENO); // 将屏幕输入,重定向给 fd1 所指向的文件

    printf("-----------------------886");

    close(fd1);
    close(fd2);

    return 0;
}

5.4 fcntl 实现 dup

当 fcntl 的第二个参数为 F_DUPFD 时, 它的作用是根据一个已有的文件描述符,复制生成一个新的文件描述符。此时,fcntl 相当于 dup 和 dup2 函数。

参 3 指定为 0 时,因为 0 号文件描述符已经被占用。所以函数自动用一个最小可用文件描述符。

参 3 指定为 9 时,如果该文件描述符未被占用,则返回 9。否则,返回大于 9 的可用文件描述符。

fcntl_dup.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.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);

    int ret = write(newfd2, "YYYYYYY", 7);
    printf("ret = %d\n", ret);

    close(fd1);

    return 0;
}

 

相关链接

教程视频:Linux系统编程哔哩哔哩bilibili

Linux系列文章:Linux – Echo (liveout.cn)

GCC、GDB、Makefile:GCC、GDB、Makefile学习笔记 – Echo (liveout.cn)

Linux系统编程1:文件I/O笔记:Linux系统编程1:文件I/O

GitHub仓库,包含教程讲义、代码以及笔记:https://github.com/PGwind/LinuxSystem

--> 前言此篇文章为学习 Linux系统编程02:文件系统 部分的笔记1. 文件存储1.1 inodeinode 是 Linux 和 Unix 操作系统中的一个重要概念,它是文件系统中的一个 数据结构,用于存储文件的元数据。每个文件和目录都有一个对应的 inode 来描述其属性和位置信息。root@freecho:/opt/C/gcc/code# stat hello.c File: hello...

前言

此篇文章为学习 Linux系统编程01:文件I/O 部分的笔记

1. open/close函数

1.1 open

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

flags

flags 用于指定文件的打开/创建模式,这个参数可由以下三个互斥的常量(定义于 fcntl.h)通过逻辑或(|)连接:

O_RDONLY      只读模式 
O_WRONLY      只写模式 
O_RDWR        读写模式

其他可选常量:

常量含义
O_APPEND每次写操作都写入文件的末尾
O_CREAT如果指定文件不存在,则创建这个文件
O_EXCL如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY如果路径名指向终端设备,不要把这个设备用作控制终端
O_NONBLOCK如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)

以下用于同步输入输出

常量含义
O_DSYNC等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新
O_RSYNCread 等待所有写入同一区域的写操作完成后再进行
O_SYNC等待物理 I/O 结束后再 write,包括更新文件属性的 I/O

 

mode

mode 和 fopen() 函数的 mode 参数相同。mode 指定文件的打开模式:

r = 4,w = 2, x = 1

  • r:只读方式打开一个文本文件(该文件必须存在)
  • r+:可读可写方式打开一个文本文件(该文件必须存在)
  • w:只写方式打开一个文本文件(若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件)
  • w+:可读可写方式创建一个文本文件(若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件)
  • a:追加方式打开一个文本文件(若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留))
  • a+:可读可写追加方式打开一个文本文件(若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留))

a 和 a+ 的区别:a 不能读,a+ 可以读

  • rb:只读方式打开一个二进制文件(使用法则同r)
  • rb+:可读可写方式打开一个二进制文件(使用法则同r+)
  • wb:只写方式打开一个二进制文件(使用法则同w)
  • wb+:可读可写方式生成一个二进制文件(使用法则同w+)
  • ab:追加方式打开一个二进制文件(使用法则同a)
  • ab+:可读可写方式追加一个二进制文件(使用法则同a+)

需要注意的是,当 flags 为 O_CREAT 或 O_TMPFILE 时,必须提供 mode 参数;否则 mode 参数将不起作用。

返回值
open() 的返回值是一个 int 类型的文件描述符,打开失败返回 -1。

 

1.2 close

NAME
close - 关闭一个文件描述符

总览

 #include <unistd.h> 
int close(int fd);

描述
close 关闭 一个文件描述符 , 使不在指向任何文件和可以在新的文件操作中被再次使用

任何与此文件相关联的以及程序所拥有的锁, 都会被删除 (忽略那些持有锁的文件描述符)

假如 fd 是最后一个文件描述符与此资源相关联 , 则这个资源将被释放.  
若此描述符是最后一个引用到此文件上的, 则文件将使用 unlink(2) 删除.

返回值

close 返回 0 表示 成功 , 或者 -1 表示 有 错误 发生 .

错误信息

EBADF  fd 不是 一个 有效 的 已 被 打开 的 文件 的 描述符

EINTR  The close() 调用 被 一 信号 中断.

EIO    I/O 有 错误 发生

 

1.3 文件权限

在Linux系统中,文件的最终权限是由文件的创建模式(mode)和用户掩码(umask)共同决定的。

文件的创建模式(mode)是指在使用系统调用如open()creat()创建文件时,通过指定一个八进制数来表示文件的权限。通常使用三个数字来表示权限,分别对应所有者、所属组和其他用户的权限。每个数字由三个位组成,分别代表读(r)、写(w)和执行(x)权限。例如,权限为 rw-r--r-- 的文件模式用八进制表示就是 644

用户掩码(umask)是用来屏蔽(取消)文件创建模式中的某些权限。默认情况下,umask 设置为 022,表示屏蔽掉写权限(w)和执行权限(x)对其他用户。所以如果文件的模式是 644,经过 umask 的处理,其他用户的权限就变为 444,即只有读权限。

计算文件的最终权限可以使用如下公式:

最终权限 = 文件模式(mode) & (~用户掩码(umask))

例如,假设文件的模式是 755,umask 是 022,则最终权限为:

755 & (~022) = 755 & 755 = 755

所以最终权限仍然是 755

另外,需要注意的是,umask 的值通常是用八进制数表示的。在设置 umask 时,一般使用四个数字,例如 umask 022

root@freecho:/opt/C/file_IO/test# umask
0022  # 755

 

1.4 代码样例

open1.c

读取文件,文件存在则以只读 O_RDONLY 方式打开,并且清空文件全部内容 O_TRUNC,如果不存在则创建文件 O_CREAT

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
    int fd;
    // 0644 第一个0表示8进制, 644为权限
    fd = open("./dict.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // 644 rw-r--r--

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

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}

/*
root@freecho:/opt/C/file_IO/test# ./open1
fd = 3
*/
root@freecho:/opt/C/file_IO/test# ./open1
fd = 3

 

open2.c

打开不存在的文件

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
    int fd;
    fd = open("./dict1.cp", O_RDONLY); 

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

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}
root@freecho:/opt/C/file_IO/test# ./open2
fd = -1, errno = 2:No such file or directory

 

open3.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
    int fd;

    fd = open("mydir", O_WRONLY); // 打开文件夹

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

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}
root@freecho:/opt/C/file_IO/test# ./open3
fd = -1, errno = 21:Is a directory

 

1.5 错误及处理

open 常见错误:

  1. 打开文件不存在
  2. 以写方式打开只读文件(权限问题)
  3. 以只写方式打开目录
  4. 当 open 出错时,程序会自动设置 errno,可以通过 strerror(errno)来查看报错数字的含义

错误处理函数

  1. printf("xxx error: %d\n", errno);
    
  2. char *strerror(int errnum);
    	printf("xxx error: %s\n", strerror(errno));
    
  3. void perror(const char *s);
    	perror("xxx error");
    

 

2. read/write函数

2.1 read

Man手册

NAME
read - 在文件描述符上执行读操作

概述

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

描述

read( ) 从文件描述符 fd 中读取 count 字节的数据并放入从 buf 开始的缓冲区中.

如果 count 为零,read( )返回0,不执行其他任何操作. 如果 count 大于SSIZE_MAX,那么结果将不可预料.

返回值

成功时返回读取到的字节数(为零表示读到文件描述符), 此返回值受文件剩余字节数限制

当返回值小于指定的字节数时 并不意味着错误;这可能是因为当前可读取的字节数小于指定的 字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数 据,或者read( )被信号中断)

发生错误时返回-1,并置 errno 为相应值.在这种情况下无法得知文件偏移位置是否有变化.

错误代码

EINTR  在读取到数据以前调用被信号所中断.

EAGAIN 使用 O_NONBLOCK 标志指定了非阻塞式输入输出,但当前没有数据可读.

EIO    输入输出错误.可能是正处于后台进程组进程试图读取其 控制终端,但读操作无效,或者被信号SIGTTIN所阻塞, 或者其进程组是孤儿进程组.也可能执行的是读磁盘或者 磁带机这样的底层输入输出错误.

EISDIR fd 指向一个目录.

EBADF  fd 不是一个合法的文件描述符,或者不是为读操作而打开.

EINVAL fd 所连接的对象不可读.

EFAULT buf 超出用户可访问的地址空间.

也可能发生其他错误,具体情况和 fd 所连接的对象有关.  POSIX 允许  read  在读取了一定量的数据后被信号所中断,并返回  -1(且  errno  被设置为EINTR),或者返回已读取的数据量.

2.2 write

Man手册

NAME
write -在一个文件描述符上执行写操作

概述

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

描述
write 向文件描述符 fd 所引用的文件中写入 从 buf 开始的缓冲区中 count 字节的数据.

POSIX规定,当使用了write()之后再使用 read(),那么读取到的应该是更新后的数据. 但请注意并不是所有的文件系统都是 POSIX兼容的.

返回值
成功时返回所写入的字节数(若为零则表示没有写入数据).

错误时返回-1,并置errno为相应值.

若count为零,对于普通文件无任何影响,但对特殊文件将产生不可预料的后果.

错误代码

EBADF  fd 不是一个合法的文件描述符或者没有以写方式打开.

EINVAL fd 所指向的对象不可写.

EFAULT buf 不在用户可访问地址空间内.

EPIPE  fd  连接到一个管道,或者套接字的读方向一端已关闭.此时写进程  将接收到  SIGPIPE  信号;如果此信号被捕获,阻塞或忽略,那么将返回错误EPIPE.

EAGAIN 读操作阻塞,但使用 O_NONBLOCK 指定了非阻塞式输入输出.

EINTR  在写数据以前调用被信号中断.

ENOSPC fd 指向的文件所在的设备无可用空间.

EIO    当编辑一个节点时发生了底层输入输出错误.

可能发生了其他错误,取决于 fd 所连接的对象.

2.3 代码样例

mycp.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

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

    int n =0;

    int fd1 = open(argv[1], O_RDONLY); // read
    if (fd1 == -1) {
        perror("open argv1 error");
        exit(1);
    }

    int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664); // rw-rw--r
    if (fd1 == -1) {
        perror("open argv2 error");
        exit(1);
    }

    // read 返回为读取到的字节数
    while ((n = read(fd1, buf, 1024)) != 0) { // 0:读到文件结尾结束循环
        if (n < 0) {
            perror("read error");
            break;
        }
        write(fd2, buf, n);
    }

    close(fd1);
    close(fd2);

    return 0;
}
root@freecho:/opt/C/file_IO/rw_test# make
gcc mycp.c -o mycp
root@freecho:/opt/C/file_IO/rw_test# ls
makefile  mycp  mycp.c  test1.txt
root@freecho:/opt/C/file_IO/rw_test# ./mycp test1.txt test2.txt
root@freecho:/opt/C/file_IO/rw_test# ls
makefile  mycp  mycp.c  test1.txt  test2.txt
root@freecho:/opt/C/file_IO/rw_test# cat test2.txt
Hello 这是一个cp测试文件
root@freecho:/opt/C/file_IO/rw_test# ./mycp 123
open argv1 error: No such file or directory

 

2.4 strace命令

strace是一个用于跟踪和分析程序系统调用的工具。它在Linux系统上广泛使用,可用于诊断程序执行时的问题,分析程序与操作系统之间的交互,以及定位程序的错误和性能瓶颈。strace可以帮助开发人员和系统管理员深入了解程序的运行情况,包括系统调用、信号、文件操作、网络通信等。

使用strace命令时,它会启动被跟踪的目标程序,并输出程序执行过程中的系统调用和信号等相关信息。可以用于追踪应用程序、脚本、甚至是其他系统命令的执行。

常见用法:

strace <command>

示例:

strace -o output.txt ls  # 将输出重定向到文件
strace -e trace=open,read,write ls  # 只跟踪指定的系统调用
strace -p <PID>  # 跟踪正在运行的进程

strace的输出会显示程序执行过程中每个系统调用的结果、参数和返回值,以及相应的错误信息。通过分析strace的输出,可以帮助定位程序运行时的问题,识别潜在的错误或性能瓶颈,进而进行适当的优化和调试。

 

2.5 预读入缓输出

通过比较 fgetc/fputcread/write 的执行速度,了解预读入缓输出

库函数: getc_cmp_read.c

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *fp, *fp_out;
    int n;

    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        perror("fopen error");
        exit(1);
    }

    fp_out = fopen("test.cp", "w");
    if (fp_out == NULL) {
        perror("fopen error");
        exit(1);
    }

    while ((n = fgetc(fp)) != EOF) {
        fputc(n, fp_out);
    }

    fclose(fp);
    fclose(fp_out);

    return 0;
}

系统调用:read_cmp_getc.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define N 1

int main(int argc, char *argv[])
{
    int fd, fd_out;
    int n;
    char buf[N];

    fd = open("test.txt", O_RDONLY); // read
    if (fd < 0) {
        perror("open test.txt error");
        exit(1);
    }

    fd_out = open("test.cp", O_WRONLY | O_CREAT | O_TRUNC, 0664); // rw-rw--r
    if (fd_out < 0) {
        perror("open test.cp error");
        exit(1);
    }

    // read 返回为读取到的字节数
    while ((n = read(fd, buf, N))) { // 0:读到文件结尾结束循环
        if (n < 0) {
            perror("read error");
            break;
        }
        write(fd_out, buf, n);
    }

    close(fd);
    close(fd_out);

    return 0;
}
strace -o read.txt ./read_cmp_getc
strace -o getc.txt ./getc_cmp_read
  1. readwrite系统调用是无用户缓冲区的,它们直接在用户空间和内核空间之间传递数据,每次操作只处理一个字节或一组字节,需要频繁地在用户空间和内核空间之间切换,因此在处理大量数据时可能会比较慢。
  2. fgetcfputc是C标准库提供的函数,它们使用了用户缓冲区。C标准库在打开文件时会为文件分配一个缓冲区,当使用fgetc读取一个字符或使用fputc写入一个字符时,实际上是将数据读取到用户缓冲区或从用户缓冲区写入数据。当用户缓冲区满了或遇到fflushfclose等情况时,数据才会被传输到内核。
  3. 在大多数情况下,fgetcfputc的性能相对于readwrite会慢一些,因为它们多了一层用户缓冲区的处理。但在某些情况下,特别是频繁读写小量数据时,使用C标准库提供的函数可以减少系统调用的次数,从而提高性能。

综上所述,readwrite是直接的系统调用,没有用户缓冲区,适用于大量数据的读写;而fgetcfputc是C标准库提供的函数,使用了用户缓冲区,适用于频繁读写小量数据。选择使用哪种方法取决于具体的应用场景和性能需求。

3. 文件描述符

此部分主要由 chatGPT 生成讲解

3.1 PCB进程控制块

PCB(Process Control Block,进程控制块)是操作系统中用于管理和维护进程信息的 数据结构。每个运行的进程都对应一个唯一的PCB,PCB记录了进程的状态、标识符、优先级、寄存器值、程序计数器、内存分配信息、打开的文件、CPU占用时间等信息。

PCB是操作系统实现多道程序设计和进程调度的基础。当一个进程被调度执行时,操作系统会将当前进程的上下文(包括寄存器状态等)保存在该进程的PCB中,然后加载要执行的进程的上下文,使得新进程可以继续执行。当操作系统决定暂停或切换进程时,会再次保存当前进程的上下文到其PCB中,同时加载下一个要执行的进程的上下文。

PCB 实际上是一个结构体:struct task_struct { 结构体

PCB 通常包含以下字段:

  1. 进程标识符(Process ID,PID):用于唯一标识进程的整数值。
  2. 程序计数器(Program Counter,PC):指向当前执行指令的地址。
  3. 寄存器值:保存进程的寄存器状态,包括通用寄存器、程序状态字(PSW)等。
  4. 进程状态:表示进程的当前状态,如运行、就绪、阻塞等。
  5. 进程优先级:用于进程调度时确定进程的优先级顺序。
  6. 内存分配信息:记录进程占用的内存地址空间。
  7. 打开的文件列表:记录进程打开的文件及其 文件描述符
  8. CPU占用时间:记录进程在CPU上执行的时间。
  9. 其他控制信息:可能包含信号量、消息队列、进程间通信等信息。

PCB在操作系统中起着重要的作用,它是实现多任务、多进程的基础,也是操作系统对进程进行管理和调度的核心数据结构。每个进程都有一个对应的PCB,当进程从运行态切换到阻塞态或就绪态时,PCB中的信息会被更新和保存,以便在合适的时候重新调度和恢复进程的执行。

 

3.2 文件描述符表

文件描述符

文件描述符是在操作系统中用于标识打开文件的整数值。在Unix-like系统中,包括Linux,每个打开的文件都会被分配一个文件描述符,用于标识该文件。文件描述符是一个非负整数,通常由操作系统管理。

在C语言中,使用int类型来表示文件描述符。文件描述符的值为0、1和2通常有特殊意义:

  • 文件描述符0表示标准输入(stdin),它是程序从终端接收输入数据的文件描述符。
  • 文件描述符1表示标准输出(stdout),它是程序向终端输出数据的文件描述符。
  • 文件描述符2表示标准错误(stderr),它用于向终端输出错误信息。

除了标准输入、标准输出和标准错误外,程序还可以通过系统调用(例如openreadwrite等)打开其他文件,从而得到相应的文件描述符。每个新打开的文件都会获得一个尚未使用的最小的非负整数值作为其文件描述符。

文件描述符的主要作用是用于标识文件,从而在程序中进行文件的读取、写入和关闭等操作。在C语言中,通常使用文件描述符来操作文件,例如使用readwrite来读写文件内容,使用close来关闭文件。文件描述符是一个重要的概念,它使得程序可以方便地管理多个打开的文件,并进行相应的文件操作。

文件描述符表

文件描述符表是操作系统中用于管理进程打开的文件的数据结构。在Unix-like操作系统中,每个进程都有一个文件描述符表,它是一个数组,其中的 每个元素都是一个文件描述符

数组下标可以看成指针,指向文件结构体struct file

struct file {
    
}

操作系统隐藏文件结构体内容,只暴露 数组下标,即文件描述符给用户

文件描述符表的大小是由操作系统预先定义的,并且有一定的限制。在Linux系统中,通常默认情况下,每个进程最多可以打开 1024 个文件,这个限制可以通过修改系统参数来调整。

当进程执行一个新程序时,它会继承原有的文件描述符表。这意味着在新程序中也可以继续使用原有的文件描述符来操作文件。

文件描述符表是操作系统管理进程文件操作的重要机制,它使得进程可以方便地访问和操作打开的文件,从而实现了进程与文件之间的交互和通信。

 

3.3 最大打开文件数

最大打开文件数是操作系统对一个进程能够同时打开的文件数量进行限制的值。这个限制是为了保证系统资源的合理分配和控制,防止某个进程滥用资源导致系统资源耗尽。

在Unix-like系统中,包括Linux和macOS等,最大打开文件数由系统参数ulimit来控制。ulimit命令用于设置或显示用户的资源限制,其中包括最大打开文件数。

要查看当前用户的最大打开文件数,可以在终端中运行以下命令:

ulimit -n

通常情况下,ulimit -n 的默认值为1024,即每个进程最多可以同时打开1024个文件。这个值对于普通用户来说已经足够了,但对于某些特殊应用或服务器程序来说可能会不够。

如果需要增加最大打开文件数的限制,可以使用 ulimit 命令进行设置,但是普通用户通常只能增加到一定的限制,超过系统默认值需要管理员权限。

在Linux系统中,还可以通过修改系统配置文件来增加最大打开文件数的限制。这个配置文件通常是 /etc/security/limits.conf,可以在其中添加类似下面的配置:

*       soft    nofile  65535
*       hard    nofile  65535

这里的 65535 是新的最大打开文件数限制值。设置后需要重新登录或重启系统才能生效。

需要注意的是,增加最大打开文件数的限制会占用更多的系统资源,因此在修改配置时要慎重考虑,确保系统有足够的资源支持。不当的配置可能会导致系统性能下降或资源耗尽问题。

 

3.4 FILE结构体

FILE结构体是C标准库中用于文件操作的重要数据结构。它定义在 stdio.h 头文件中,并由C库提供文件读写函数所使用。

在标准C库中,FILE结构体的定义如下:

typedef struct _IO_FILE FILE;

具体的FILE结构体的定义会因不同的编译器和操作系统而有所不同,但通常会包含用于管理文件操作的各种成员变量。

FILE结构体的主要作用是用于维护文件的状态信息,例如文件指针位置、缓冲区、读写模式等。通过FILE结构体,C库能够实现对文件的高效读写操作,并且对于程序员来说,无需直接处理底层的文件描述符,只需使用FILE指针即可。

C标准库中提供了许多基于FILE结构体的文件读写函数,例如:

  • fopen:打开文件并返回一个FILE指针。
  • fclose:关闭文件。
  • fgetc、getc:从文件中读取一个字符。
  • fgets:从文件中读取一行字符串。
  • fputc、putc:将一个字符写入文件。
  • fputs:将字符串写入文件。
  • fprintf:格式化输出到文件。
  • fread、fwrite:二进制读写数据。
  • fseek、ftell:文件指针定位。

使用这些函数,可以方便地进行文件的读写操作,而无需直接操作文件描述符。当然,底层仍然是通过文件描述符来进行实际的文件读写操作,但这一过程对于程序员来说是透明的。

需要注意的是,在使用标准C库的文件读写函数时,要注意及时关闭文件以释放资源,并检查函数返回值以处理可能出现的错误情况。

 

4. 阻塞、非阻塞

4.1 概念

读常规文件是不会阻塞的,不管读多少字节,read 一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用 read 读终端设备就会阻塞,如果网络上没有接收到数据包,调用 read 从网络读就会阻塞,至于会阻塞多长时间 也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的, 而向终端设备或网络写则不一定。

现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在 Linux 内核中,处于运行状态的进程分为两种情况:

  1. 正在被调度执行:CPU 处于该进程的上下文环境中,程序计数器(eip)里保存着该进 程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令, 正在读写该进程的地址空间。
  2. 就绪状态:该进程不需要等待什么事件发生,随时都可以执行,但 CPU 暂时还在执行 另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的 进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进 程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同 时要兼顾用户体验,不能让和用户交互的进程响应太慢。

阻塞、非阻塞是设备文件、网络文件的属性

产生阻塞的场景:读设备文件、读网络文件。(读常规文件无阻塞概念) 补:/dev/tty --- 终端文件

注意,阻塞与非阻塞是对于文件而言的。而不是 read、write 等的属性。read 终端,默认阻塞读。

 

4.2 代码样例

block_readtty.c:阻塞阻塞读终端

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    char buf[10];
    int n;

    // #define STDIN_FILENO 0  STDOUT_FILENO 1  STDERR_FILENO 2
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
        perror("read STDIN_FILENO");
        // printf("%d", errno);
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);

    return 0;
}

nonblock_readtty.c:非阻塞读终端,通过 open 函数 改变文件状态

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    char buf[10];
    int fd, n;

    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); // 设置非阻塞状态
    if (fd < 0) {
        perror("open /dev/tty");
        exit(1);
    }

tryagain:

    n = read(fd, buf, 10);

    // 如果read返回-1,并且 errno = EAGIN 或 EWOULDBLOCK,
    // 说明不是read失败,而是read在以非阻塞方式读一个设备或网络文件,并且文件无数据
    if (n < 0) {
        if (errno != EAGAIN) {        // if (errno != EWOULDBLOCK)
            perror("read /div/tty");
            exit(1);
        } else {
            write(STDOUT_FILENO, "try again\n", strlen("try again\n"));
            sleep(2);
            goto tryagain;
        }
    }

    write(STDOUT_FILENO, buf, n);
    close(fd);

    return 0;
}

nonblock_timeout.c:非阻塞读终端和等待超时

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"

int main()
{
    char buf[10];
    int fd, n, i;

    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); // 设置非阻塞状态
    if (fd < 0) {
        perror("open /dev/tty");
        exit(1);
    }
    printf("open /dev/tty ok ... %d\n", fd);

    for (i = 0; i < 5; i++) {
        n = read(fd, buf, 10);
        if (n > 0) {               // 说明读到了东西
            break;
        }
        if (errno != EAGAIN) {          // if (errno != EWOULDBLOCK)
            perror("read /dev/tty");
            exit(1);
        } else {
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            sleep(2);
        }
    }

    if (i == 5) {
            write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    } else {
        write(STDOUT_FILENO, buf, n);
    }

    close(fd);

    return 0;
}

 

5. fcntl 函数

5.1 概念

fcntl 函数是用于操作文件描述符的系统调用,它可以用来执行各种与文件描述符相关的操作。在C语言中,我们可以通过 fcntl 函数来调用这个系统调用。

函数原型

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

参数说明

  • fd:要操作的文件描述符。

  • cmd:指定要执行的操作,可以是下列常量之一:


    • F_DUPFD:复制文件描述符。
    • F_GETFD:获取文件描述符标志。
    • F_SETFD:设置文件描述符标志。
    • F_GETFL:获取文件状态标志。
    • F_SETFL:设置文件状态标志。
    • F_GETOWN:获取异步I/O所有权。
    • F_SETOWN:设置异步I/O所有权。
    • F_GETLK:获取文件锁信息。
    • F_SETLK:设置文件锁。
    • F_SETLKW:设置文件锁,但如果锁不可用,则阻塞。
  • arg:根据操作类型 cmd 的不同,可能需要传入其他参数。

返回值

  • 如果执行成功,则根据操作类型 cmd 的不同,返回值也不同。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误类型。

 

5.2 代码样例

下面以 F_GETFLF_SETFL 操作为例,演示如何使用 fcntl 函数来获取和设置文件状态标志:

fcntl_1.c

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    int fd = open("example.txt", O_RDONLY);  // 打开文件 example.txt,只读模式

    // 获取文件状态标志
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl");
        return 1;
    }

    printf("File flags before modification: %d\n", flags);

    // 添加 O_APPEND 标志
    flags |= O_APPEND;
    
    // 设置文件状态标志
    int result = fcntl(fd, F_SETFL, flags);
    if (result == -1) {
        perror("fcntl");
        return 1;
    }

    printf("File flags after modification: %d\n", flags);

    close(fd);
    return 0;
}

在这个例子中,我们首先打开了一个文件 example.txt,然后使用 fcntl 函数获取文件状态标志,接着添加 O_APPEND 标志,最后再次使用 fcntl 函数将修改后的标志设置回去。运行此程序,可以看到文件状态标志的变化。

下面再来一个例子

fcntl_2.c:将文件改为非阻塞状态

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

#define MSG_TRY "try again\n"

int main(void)
{
    char buf[10];
    int flags, n;

    flags = fcntl(STDIN_FILENO, F_GETFL); // 获取 stdin 属性信息

    if (flags == -1) {
        perror("fcntl error");
        exit(1);
    }

    flags |= O_NONBLOCK; // 添加上非阻塞状态
    int ret = fcntl(STDIN_FILENO, F_SETFL, flags);

    if (ret == -1) {
        perror("fcntl error");
        exit(1);
    }

tryagain:
    n = read(STDIN_FILENO, buf, 10);

    if (n < 0) {
        if (errno != EAGAIN) {
            perror("read /dev/tty");
            exit(1);
        }
        sleep(3);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        goto tryagain;
    }

    write(STDOUT_FILENO, buf, n);

    return 0;
}

 

6. lseek函数

6.1 概念

lseek 函数用于在打开的文件中定位文件指针的位置。它在 C 语言中是一个系统调用,提供了对文件读写位置的控制。

函数原型

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

参数说明

  • fd:文件描述符,是文件在进程中的标识符。

  • offset:指定了文件指针的偏移量。这个值可以为正、负或零,具体取决于 whence 参数。

  • whence:用于确定 offset 如何解释,它可以取以下值:


    • SEEK_SET:从文件开头开始偏移。
    • SEEK_CUR:从当前文件指针位置开始偏移。
    • SEEK_END:从文件末尾开始偏移。

返回值

lseek 函数返回新的文件指针位置,若出错则返回 -1,并设置全局变量 errno 来表示错误类型。

特别的:lseek 允许超过文件结尾设置偏移量,文件会因此被拓展。

注意文件“读”和“写”使用同一偏移位置。

 

6.2 代码样例

lseek1.c :文件读写使用同一偏移位置

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

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

    fd = open("test1.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);
        }
        write(STDOUT_FILENO, &ch, n); // 将文件内容按字节读出,写出到屏幕
    }

    close(fd);

    return 0;
}

lseek2.c :获取文件大小

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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 lenth = lseek(fd, 0, SEEK_END);
    printf("file size: %d\n", lenth);

    close(fd);

    return 0;
}

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

也可以使用 truncate函数 直接拓展文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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 lenth = lseek(fd, 110, SEEK_END);
    printf("file size: %d\n", lenth);

    write(fd, "\0", 1);


    close(fd);

    return 0;
}

补充:

od -tcx filename # 查看文件的 16 进制表示形式 
od -tcd filename # 查看文件的 10 进制表示形式

 

7. ioctl.c 函数 (嵌入式)

7.1 概念

ioctl 是一个在 UNIX/Linux 系统中用于设备控制的函数。它用于与设备驱动程序进行通信,进行设备的配置、状态查询和控制等操作。

函数原型

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

参数解释

  • fd:文件描述符,用于指定要控制的设备。
  • request:一个无符号长整型参数,表示控制请求的编号。通常使用预定义的常量或者自定义的控制码。
  • ...:可选参数,如果 request 需要传递额外的数据,可以使用可变参数传递。

工作原理

ioctl 函数的作用是向设备驱动程序发送特定的控制命令(request),然后设备驱动程序根据接收到的命令执行相应的操作。这些操作可能包括设备的配置、状态查询、性能优化等。不同的设备驱动程序支持的控制命令是不同的,因此在使用 ioctl 函数时,需要参考设备驱动程序的文档或者相关头文件来了解可用的控制命令。

使用场景

ioctl 函数通常用于与特殊设备进行交互,例如硬件设备(如串口、USB 设备等)、网络设备、字符设备、以及其他类型的外设等。通过 ioctl 函数,用户可以向设备驱动程序发送自定义的控制命令,从而实现设备的灵活控制和配置。

注意事项

使用 ioctl 函数需要小心,因为它是一个较为底层的接口,如果使用不当,可能导致系统崩溃或不稳定。在使用 ioctl 函数时,建议查阅相关的文档和资料,了解设备驱动程序支持的控制命令和参数,以及正确的使用方法。

 

7.2 代码样例

ioctl.c

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>

int main() {
    int fd = open("/dev/ttyS0", O_RDWR); // 打开串口设备文件

    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 设置串口波特率为 9600
    int baud_rate = 9600;
    if (ioctl(fd, TIOCSBRSRATE, &baud_rate) < 0) {
        perror("ioctl");
        return 1;
    }

    // 关闭设备文件
    close(fd);
    return 0;
}

 

8. 传入传出参数

在编程中,函数的参数可以分为 传入参数(Input Parameter)传出参数(Output Parameter)传入传出参数(Input/Output Parameter) 三种类型。

这些参数的区别在于函数如何使用它们以及对它们的修改和返回。

8.1 传入参数

  1. 指针作为函数
  2. 通常有 const 关键字修饰
  3. 指针指向有效区域,在函数内部做读操作

8.2 传出参数

  1. 指针作为函数
  2. 在函数调用之前,指针指向的空间可以无意义,但必须有效
  3. 在函数内部,做写操作
  4. 函数调用结束后,充电函数返回值

8.3 传入传出参数

  1. 指针作为函数
  2. 在函数调用之前,指针指向的空间有实际意义
  3. 在函数内部,先做读操作,后做写操作
  4. 函数调用结束后,充当函数返回值

 

9. 扩展阅读

关于虚拟 4G 内存的描述和解析

此部分内容可以在学到 进程 时观看

一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了 4G。而用到的内存区域,会通过页表映射到物理内存

所以每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是不同的。内核用的是 3G 以上的 1G 虚拟内存地址

其中896M 是直接映射到物理地址的,128M 按需映射 896M 以上的所谓高位内存。各进程使用的是同一个内核。

首先要分清 可以寻址实际使用 的区别。

其实我们讲的每个进程都有 4G 虚拟地址空间,讲的都是“可以寻址”4G,意思是虚拟地址的 0-3G 对于一个进程的用户态和内核态来说是可以访问的,而 3-4G 是只有进程的内核态可以访问的。并不是说这个进程会用满这些空间。

其次,所谓“独立拥有的虚拟地址”是指对于每一个进程,都可以访问自己的 0-4G 的 虚拟地址。虚拟地址是“虚拟”的,需要转化为“真实”的物理地址。

好比你有你的地址簿,我有我的地址簿。你和我的地址簿都有 1、2、3、4 页,但是每 页里面的实际内容是不一样的,我的地址簿第 1 页写着 3 你的地址簿第 1 页写着 4,对于你、 我自己来说都是用第 1 页(虚拟),实际上用的分别是第 3、4 页(物理),不冲突。

内核用的 896M 虚拟地址是直接映射的,意思是只要把虚拟地址减去一个偏移量(3G) 就等于物理地址。同样,这里指的还是寻址,实际使用前还是要分配内存。而且 896M 只是 个最大值。如果物理内存小,内核能使用(分配)的可用内存也小。

 

相关链接

教程视频:Linux系统编程哔哩哔哩bilibili

Linux系列文章:Linux – Echo (liveout.cn)

Linux基础命令:Linux学习资料分享 – Echo (liveout.cn)

GCC、GDB、Makefile:GCC、GDB、Makefile学习笔记 – Echo (liveout.cn)

Vim配置:Linux学习资料分享 – Echo (liveout.cn)

GitHub仓库,包含教程讲义、代码以及笔记:https://github.com/PGwind/LinuxSystem

--> 前言此篇文章为学习 Linux系统编程01:文件I/O 部分的笔记1. open/close函数1.1 open#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int open(const char *pathname, int flags);int open(const char ...

前言

关于Linux系统编程的前导课程学习笔记,共涉及 GCC GDB Makefile 三个部分

1. GCC

介绍

GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由 Richard Stallman 于 1985 年开始开发。

GCC 原名为 GNU C语言编译器,因为它原本只能处理 C 语言,但如今的 GCC 不仅可以编译 C、C++ 和 Objective-C,还可以通过不同的前端模块支持各种语言,包括 Java、Fortran、Ada、Pascal、Go 和 D 语言等等。

GCC 的编译过程可以划分为四个阶段:预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)以及链接(Linking)。

1.1 相关参数

4个步骤 :预处理、编译、汇编、链接

-E:仅执行预处理           # hello.i  源文件 i  
-S:只编译                # hello.s  汇编文件 s 
-c:编译和汇编,但不链接。   # hello.o  目标文件 o
-o <file>:指定输出文件。   # a.out  可执行文件
-I:  指定头文件位置         # gcc -I ./inc hello.c -o hello
-g:  编译时增加调试文件
-Wall: 显示所有警告信息
-D:    向程序中“动态“注册宏定义

1.2 静态库和动态库

静态库

空间大,速度块,基本淘汰

1. gcc -c foo.c -o foo.o    # 生成 foo.o 目标文件
2. ar rcs libfoo.a foo.o    # 生成 libfoo.a 静态库
3. gcc hello.c -static libfoo.a -o hello    # 编译 并链接静态库 libfoo.a
4. gcc test.c -static ./lib/libmymath.a -I ./inc -o b.out  # 文件+库+头文件

动态库

空间小,速度慢,最多使用

1. gcc -c add.c -o add.o -fPIC          # 生成于位置无关的代码 -fPIC
2. gcc -shared -o libmymath.so add.o sub.o   #  制作动态库
3. -l: 指定库名      -L:指定库路径
   gcc test.c -lmymath -L./lib -I./inc -o a.out  # 编译可执行程序

执行报错
原因

  1. 链接器:工作于链接阶段,工作时需要 -l 和 -L
  2. 动态链接器:工作于程序运行阶段,工作时需要提供动态库所在目录位置解决

解决 :

  1. export LD_LIBRARY_PATH=动态库路径:$LD_LIBRARY_PATH (一次进程里,临时)

  2. 移动到 /usr/lib (永久)

  3. .bashrc 中 写入 环境变量 (永久)

  4. vim /etc/ld.so.conf  # 写入动态库的路径
    ldconfig  #  刷新  (永久)
    

ldd是Linux系统下的一个命令行工具,用于查看可执行程序或共享库所依赖的动态链接库(Dynamic Linking Library,简称DLL)。它会列出指定的可执行程序或共享库所使用的共享库,并显示它们的完整路径。

root@freecho:/opt/C/gcc/lib/dynamicLib# ldd a.out
        linux-vdso.so.1 (0x0000ffffb6b6d000)
        libmymath.so => /lib/libmymath.so (0x0000ffffb6af0000)
        libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffffb6940000)
        /lib/ld-linux-aarch64.so.1 (0x0000ffffb6b34000)
root@freecho:/opt/C/gcc/lib/dynamicLib#

 

2. GDB

介绍

GDB 全称“GNU symbolic debugger”,从名称上不难看出,它诞生于 GNU 计划(同时诞生的还有 GCC、Emacs 等),是 Linux 下常用的程序调试器。发展至今,GDB 已经迭代了诸多个版本,当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada 等。实际场景中,GDB 更常用来调试 C 和 C++ 程序

2.1 基础指令

gcc gdbtest.c -o a.out -g   # -g 使用该参数编译可执行文件,得到调试表
gdb a.out # 开始
l 1       # 从第一行开始显示
l 		  # 继续往下显示   (lsit)
b 52	  # 52行设置断点	  (breakout)
r		  # 运行 		(run)
n   	  # 逐过程单步执行,越过函数,系统函数用 n              (next)
s 		  # 逐语句单步执行,进入函数,其他情况与n无区别,执行下一步 		(step)
until 16  # 运行程序直到达到指定的行号并停止
p i       # 打印变量 i 的值  (print)
continue  # 继续执行断点后续指令
quit      # 退出gdb调试

2.2 断点指令

(gdb) b 23 # 设置断点
(gdb) b 30
(gdb) b 41
(gdb) b 41 if i=5 # 设置条件断点
(gdb) info b # 查看断点信息表
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000000009f8 in select_sort at gdbtest2.c:23
2       breakpoint     keep y   0x0000000000000a7c in select_sort at gdbtest2.c:30
3       breakpoint     keep y   0x0000000000000b14 in print_arr at gdbtest2.c:41
4       breakpoint     keep y   0x0000000000000b14 in print_arr at gdbtest2.c:41
        stop only if i=5
(gdb) delete 3 # 删除指定断点

delete  # 删除所有断点
disable	# 禁用断点
enable  # 启用断点

2.3 其他指令

  1. 使用 r 查找段错误出现位置
  2. start :用于开始执行程序并停在main函数的第一条语句上
  3. finish :当你在调试一个函数时,你可以使用finish命令来执行该函数的剩余部分,并停在函数调用的地方
  4. ser args :设置 main 函数命令行参数,在start run 命令之前
  5. run 字符串1字符串2 ... : 设置 main 函数命令行参数
  6. info b:查看断点信息表 information breakout
  7. ptype i:查看变量类型
  8. backtrace :查看函数的调用的栈帧和层级关系 缩写:bt
  9. frame 编号:切换函数的栈帧 缩写:f
  10. display i :设置跟踪变量
  11. undisplay 编号:取消跟踪变量

补:栈帧

栈帧(Stack Frame) 是在程序执行期间,用于管理函数调用和局部变量的一种数据结构。在函数调用时,会创建一个栈帧来存储函数的参数、局部变量以及保存调用函数的返回地址和调用函数的寄存器状态等信息。

每个函数调用都会创建一个新的栈帧,将其添加到函数调用栈(Call Stack)的顶部。当函数执行结束后,其对应的栈帧会被销毁,栈帧也会被从调用栈中移除,程序会返回到调用该函数的上一个栈帧继续执行。

栈帧通常包含以下几个重要的部分:

  1. 返回地址:指向调用该函数的下一条指令的地址,用于函数执行完后返回到正确的位置。
  2. 函数参数:将函数的参数值存储在栈帧中,以便在函数内部使用。
  3. 本地变量:函数中定义的局部变量也被存储在栈帧中,它们只在函数执行期间存在,函数结束后将被销毁。
  4. 保存的寄存器状态:为了保护调用函数时的寄存器状态,可能需要将某些寄存器的值保存在栈帧中,在函数执行完后再恢复寄存器的值。

栈帧的使用使得函数调用和返回的过程更加高效和可控,同时提供了函数间局部变量的隔离,避免了函数之间的冲突。栈帧在计算机体系结构和操作系统中发挥着重要作用,为函数调用提供了必要的支持。

 

3. Makefile

介绍

Makefile是一个用于构建和管理项目的工程文件,它定义了项目中的源文件、依赖关系以及构建规则等信息,用于自动化编译和链接程序。

Makefile通常使用make工具来执行其中定义的规则和命令。通过Makefile,开发人员可以在项目中定义编译、链接、清理等操作的规则,以及指定项目中的文件之间的依赖关系。这样,当项目的源文件发生变化时,make工具可以根据Makefile中的规则,自动检测变化并重新编译需要更新的文件,从而加快项目的开发和构建过程。

文件命令:makefile Makefile

1个规则

目标:依赖条件
	(一个tab缩进) 命令
  1. 目标的时间必须晚于依赖条件的时间,否则,更新目标
  2. 依赖条件如果不存在,找寻新的规则去产生依赖

ALL:指定makefile的最终目标,否则最终目标写在文件开头

步骤

root@freecho:/opt/C/make# vim makefile
root@freecho:/opt/C/make# cat makefile
hello:hello.c
        gcc hello.c -o a.out
root@freecho:/opt/C/make# ls
hello.c  makefile
root@freecho:/opt/C/make# make
gcc hello.c -o a.out
root@freecho:/opt/C/make# ls
a.out  hello.c  makefile

makefile文件1

ALL:a.out  # 指定makefile的最终目标,否则最终目标写在文件开头

hell.o:hello.c
        gcc -c hello.c -o hello.o
add.o:add.c
        gcc -c add.c -o add.o
sub.o:sub.c
        gcc -c sub.c -o sub.o
mul.o:mul.c
        gcc -c mul.c -o mul.o
div1.o:div1.c
        gcc -c div1.c -o div1.o
a.out:hello.o add.o sub.o mul.o div1.o
        gcc hello.o add.o sub.o mul.o div1.o -o a.out

2个函数

两个函数分别为:wildcard(通配符)patsubst(匹配替换)

2.1 $(wildcard ./*.c)

src = $(wildcard *.c) 
# src = add.c sub.c mul.c div1.c

匹配当前目录下的所有后缀为 .c 的文件,将文件名组成列表,赋值给变量 src

2.2 $(patsubst %.c, %.o, $(src))

obj = $(patsubst %.c, %.o, $(src))
# obj = add.o sub.o mul.o div1.o

参数3 中,包含 参数1 的部分,替换为 参数2,即把 src 变量里所有后缀为 .c 的文件替换为 .o

makefile文件2、3

src = $(wildcard *.c)               # src = add.c sub.c mul.c div1.c
obj = $(patsubst %.c, %.o, $(src))  # obj = add.o sub.o mul.o div1.o

ALL:a.out

hell.o:hello.c
        gcc -c hello.c -o hello.o
add.o:add.c
        gcc -c add.c -o add.o
sub.o:sub.c
        gcc -c sub.c -o sub.o
mul.o:mul.c
        gcc -c mul.c -o mul.o
div1.o:div1.c
        gcc -c div1.c -o div1.o
a.out: $(obj)
        gcc $(obj) -o a.out

clean:  # 终端输入 make clean 才会生效
        -rm -rf $(obj)  # -rm 前面的 '-'  表示出错依然执行

步骤

clean: (没有依赖)

make clean -n 会演示清除命令,不会实际生效

make clean 则会实际生效

root@freecho:/opt/C/make# make
gcc -c add.c -o add.o
gcc -c div1.c -o div1.o
gcc -c -o hello.o hello.c
gcc -c mul.c -o mul.o
gcc -c sub.c -o sub.o
gcc  add.o  div1.o  hello.o  mul.o  sub.o   -o a.out
root@freecho:/opt/C/make# ls
add.c  add.o  a.out  div1.c  div1.o  hello.c  hello.o  m1  m2  makefile  mul.c  mul.o  sub.c  sub.o
root@freecho:/opt/C/make# make clean -n
rm -rf  add.o  div1.o  hello.o  mul.o  sub.o
root@freecho:/opt/C/make# ls
add.c  add.o  a.out  div1.c  div1.o  hello.c  hello.o  m1  m2  makefile  mul.c  mul.o  sub.c  sub.o
root@freecho:/opt/C/make# make clean
rm -rf  add.o  div1.o  hello.o  mul.o  sub.o
root@freecho:/opt/C/make# ls
add.c  a.out  div1.c  hello.c  m1  m2  makefile  mul.c  sub.c

3个自动变量

$@ :在规则的命令中,表示规则中的目标

$< :在规则的命令中,表示规则中的第一个条件,如果将该变量应用于 模式规则 中,它可将依赖列表中的依赖依次取出,套用规则模式

$^ :在规则的命令中,表示规则中的所有依赖条件,组成一个列表,以空格隔开,如果这个列表中有重复的项则消除重复

makefile文件4

src = $(wildcard *.c)               # src = add.c sub.c mul.c div1.c
obj = $(patsubst %.c, %.o, $(src))  # obj = add.o sub.o mul.o div1.o

ALL:a.out

hell.o:hello.c
        gcc -c $< -o $@
add.o:add.c
        gcc -c $< -o $@
sub.o:sub.c
        gcc -c $< -o $@
mul.o:mul.c
        gcc -c $< -o $@
div1.o:div1.c
        gcc -c $< -o $@
a.out: $(obj)
        gcc $^ -o $@

clean:
        -rm -rf $(obj)

模式规则

编译 .c 文件 生成 .o 文件

%.o:%.c
        gcc -c $< -o $@

makefile文件5

src = $(wildcard *.c)               # src = add.c sub.c mul.c div1.c
obj = $(patsubst %.c, %.o, $(src))  # obj = add.o sub.o mul.o div1.o

ALL:a.out

a.out: $(obj)
        gcc $^ -o $@

%.o:%.c
        gcc -c $< -o $@

clean:
        -rm -rf $(obj) a.out

拓展

静态模式规则

表示对哪一个依赖条件套用这个规则

$(obj):%.o:%.c
        gcc -c $< -o $@

伪目标

声明了 .PHONY: clean ALL,它告诉 Make 不会去检查是否存在名为 cleanALL 的文件,而是直接执行它们后面的命令块。这样,运行 make cleanmake ALL 时,Make 将执行相应的命令块。

.PHONY: clean ALL

最终形态 makefile

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

myArgs = -Wall -g

ALL:a.out

a.out: $(obj)
        gcc $^ -o $@ $(myArgs)

$(obj):%.o:%.c
        gcc -c $< -o $@ $(myArgs)

clean:
        -rm -rf $(obj) a.out

.PHONY: clean ALL

相关参数

-n:模拟执行make、make clean 命令
-f:指定文件执行 make 命令

 

相关链接

教程视频:Linux系统编程哔哩哔哩bilibili

Linux系列文章:Linux – Echo (liveout.cn)

Linux基础命令:Linux学习资料分享 – Echo (liveout.cn)

Vim配置:Linux学习资料分享 – Echo (liveout.cn)

GitHub仓库,包含教程讲义、代码以及笔记:https://github.com/PGwind/LinuxSystem

--> 前言关于Linux系统编程的前导课程学习笔记,共涉及 GCC GDB Makefile 三个部分1. GCC介绍GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由 Richard Stallman 于 1985 年开始开发。GCC 原名为 GNU C语言编译器,因为它原本...

前言

在 Linux 系统中,查找文件是日常工作中常用的操作之一。Linux 提供了多个命令来帮助我们快速定位和查找文件。本文将介绍 Linux 下查找文件的几个常用命令。

 

1. find

find 命令是 Linux 中用于查找文件和目录的强大工具。它可以通过指定不同的选项来满足各种查找需求。

基本语法

find [路径] [选项] [匹配条件]

示例

查找当前目录及其子目录下所有名称为 example.txt 的文件:

find . -name "example.txt"

查找 /home/user 目录下所有以 .log 结尾的文件:

find /home/user -name "*.log"

 

2. locate

locate 命令用于快速查找文件,它基于系统维护的文件数据库进行搜索。由于它直接查询数据库,速度通常比 find 命令更快。

基本语法

locate [文件名]

示例

查找系统中所有以 .conf 结尾的配置文件:

locate *.conf

 

3. grep

grep 命令是用于在文件中搜索指定的字符串的工具,它也可以用来查找文件名。

基本语法

grep [搜索字符串] [文件名]

示例

在当前目录及其子目录下查找包含 example 字符串的文件:

grep -r "example" .

 

4. which

which 命令用于查找指定命令在系统中的安装路径。

基本语法

which [命令名]

示例

查找 ls 命令在系统中的路径:

which ls

 

5. whereis

whereis 命令用于查找二进制文件、源代码文件和帮助文档的位置。

基本语法

whereis [文件名]

示例

查找 bash 命令的二进制文件、源代码文件和帮助文档位置:

whereis bash

 

以上是 Linux 下查找文件的几个常用命令,通过灵活使用这些命令,你可以更高效地管理和查找文件,提升工作效率。

--> 前言在 Linux 系统中,查找文件是日常工作中常用的操作之一。Linux 提供了多个命令来帮助我们快速定位和查找文件。本文将介绍 Linux 下查找文件的几个常用命令。 1. findfind 命令是 Linux 中用于查找文件和目录的强大工具。它可以通过指定不同的选项来满足各种查找需求。基本语法find [路径] [选项] [匹配条件]示例查找当前目录及其子目录下所有名称为 e...

前言

 

通常查看目录的完整路径需要 pwd 命令,但是我想要直接在终端左侧显示,即如下样子

[root@VM-12-5-centos /opt/C/test]$

 

具体操作

编辑 profile 文件

vim /etc/profile

 

最后一行插入 export PS1='[\u@\h $PWD]$'

//光标最后一行
shift + g 

//编辑模式
i

//输入此命令
export PS1='[\u@\h $PWD]$'

 

刷新环境变量

source /etc/profile

 

补充

显示绝对路径

export PS1='[\u@\h $PWD]$'

只列出最后一个目录

export PS1='[\u@\h \W]$'

显示完整工作目录,当前用户目录会以$代替

export PS1='[\u@\h \w]$' 

 

--> 前言 通常查看目录的完整路径需要 pwd 命令,但是我想要直接在终端左侧显示,即如下样子[root@VM-12-5-centos /opt/C/test]$ 具体操作编辑 profile 文件vim /etc/profile 最后一行插入 export PS1='[\u@\h $PWD]$'//光标最后一行shift + g //编辑模式i//输入此命令expo...