无名管道

无名管道(Anonymous Pipe),也称为管道(Pipe),是 Linux 和类 Unix 系统中进程间通信(IPC)的一种机制。它 允许具有亲缘关系的进程(即父子进程) 通过一个特殊的文件描述符对进行数据的传输。无名管道的主要特点如下:

  1. 半双工通信:无名管道是半双工的,这意味着数据只能在一个方向上流动。一个进程可以写入数据到管道的一端(写端),而另一个进程可以从管道的另一端(读端)读取数据。

  2. 内存中的文件:无名管道实际上是在内核空间中创建的一个缓冲区,它不占用磁盘空间。这个缓冲区在内存中,因此它的速度比文件系统操作要快。

  3. 创建和使用:无名管道通常通过 pipe() 系统调用创建,这个调用会返回两个文件描述符(fd),一个用于读取,另一个用于写入。在父进程中创建管道后,通过 fork() 创建子进程,子进程会继承这些文件描述符。

  4. 数据传输:数据通过 read()write() 系统调用在管道的两端进行传输。如果读端没有数据可读,读取操作会阻塞,直到有数据写入。同样,如果写端的数据量超过了管道的缓冲区大小,写入操作也会阻塞。

  5. 管道容量:无名管道有一定的容量限制(通常为 64KB),当管道满了之后,写入操作会阻塞,直到有数据被读取出去。

  6. 信号处理:如果管道的读端没有打开,而写入操作尝试向管道写入数据,会触发 SIGPIPE 信号,导致进程终止。可以通过信号处理机制来捕获这个信号并进行相应的处理。

  7. 关闭和清理:管道的两端都应该在不再需要时关闭,以释放资源。在 fork() 之后,父进程和子进程通常需要关闭不需要的一端。

双工通信与半双工通信

双工通信(Full Duplex Communication)和半双工通信(Half Duplex Communication)是描述通信系统在数据传输时的两种工作模式。

双工通信(Full Duplex):
双工通信允许数据在两个方向上同时进行传输,即发送和接收可以同时进行,而不需要等待对方完成操作。这种通信模式类似于电话通话,双方可以同时说话和听对方说话。在计算机网络中,双工通信通常用于高速网络连接,如以太网(Ethernet)和光纤通信。

双工通信的优点是效率较高,因为它可以充分利用通信介质的带宽。在双工通信中,数据传输不需要等待对方,这减少了通信延迟。

半双工通信(Half Duplex):
半双工通信只允许数据在一个方向上传输,即在任何给定时间点,只能进行发送或接收,但不能同时进行。这类似于对讲机或早期的无线电通信,其中一方在发送信息时,另一方必须保持静默以接收信息。在计算机网络中,半双工通信的例子包括某些无线网络和早期的串行通信。

半双工通信的优点是实现相对简单,成本较低。然而,由于不能同时发送和接收数据,它的效率通常低于双工通信。在半双工通信中,可能需要额外的控制机制来协调发送和接收,以避免数据冲突。


pipe函数

pipe() 函数是 Linux 和类 Unix 操作系统中的一个系统调用,用于在两个进程之间创建一个匿名管道(无名管道),以便它们可以进行进程间通信(IPC)。这个管道允许数据在两个方向上流动,但通常用于单向通信,即一个进程向另一个进程发送数据。

函数原型如下:

#include <unistd.h>

int pipe(int fd[2]);

参数说明:

  • fd:这是一个指向包含两个整数的数组的指针。fd[0] 将用于读取数据(读端),fd[1] 将用于写入数据(写端)。

返回值:

  • 成功时,返回 0。
  • 失败时,返回 -1,并设置 errno 以指示错误类型。

使用 pipe() 创建的管道是半双工的,这意味着在任何给定时间,数据只能沿一个方向流动。例如,如果父进程向管道写入数据,子进程可以从管道读取这些数据,但不能同时向管道写入数据。

在实际应用中,通常在父进程中创建管道,然后通过 fork() 创建子进程。子进程会继承父进程的文件描述符,这样父子进程就可以通过管道进行通信。在 fork() 调用之前必须创建管道,否则子进程将不会继承这些文件描述符。

下面是一个简单的使用 pipe() 的示例:

//
// Created by ubuntu on 24-2-8.
//
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
int main() {
    int pipefd[2];
    pid_t pid;

    //创建管道
    if(pipe(pipefd) < 0) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    //创建子进程
    if((pid = fork()) < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    }else if(pid == 0) { //子进程
        //关闭写端,只保留读端
        close(pipefd[1]);
        //读取数据
        char buffer[1024];
        int n = read(pipefd[0],buffer,sizeof(buffer));
        if(n > 0) {
        	printf("Child received: %s\n",buffer);
        }
        
        //关闭读端
        close(pipefd[0]);
    }else { //父进程
        //关闭读端,只保留写端
        close(pipefd[0]);

        //向管道写入数据
        const char* message = "hello,childyyds";
        write(pipefd[1],message,strlen(message));
        //关闭写端
        close(pipefd[1]);
        //等待子进程结束 https://man.archlinux.org/man/wait.2
        wait(NULL);
    }
    return 0;
}


在这个例子中,父进程创建了一个管道,然后创建了一个子进程。子进程读取管道中的数据,而父进程向管道写入数据。这种模式常用于命令行中的管道操作,例如在 shell 中使用 grepsort 命令的组合。

有名管道

https://zhuanlan.zhihu.com/p/553377214

access函数

access 函数是一个用于检查文件或目录的访问权限的系统调用。它的原型如下:

#include <unistd.h>

int access(const char *pathname, int mode);
  • pathname:要检查权限的文件或目录的路径名。
  • mode:指定要检查的权限,可以使用以下宏的组合:
    • R_OK:检查读权限。
    • W_OK:检查写权限。
    • X_OK:检查执行权限。
    • F_OK:检查文件是否存在。

返回值:

  • 如果权限检查成功,返回值为 0。
  • 如果权限检查失败,返回值为 -1,并设置全局变量 errno 表示具体的错误类型。

注意事项:

  • access 函数用于检查实际用户的权限,而不是有效用户(effective user)的权限。
  • 检查的权限是进程的实际用户权限,而不是进程的有效用户权限。

示例:

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

int main() {
    const char *filename = "example.txt";

    // 检查文件是否存在
    if (access(filename, F_OK) == 0) {
        printf("File exists.\n");
    } else {
        perror("access");
        return 1;
    }

    // 检查文件是否可读
    if (access(filename, R_OK) == 0) {
        printf("File is readable.\n");
    } else {
        perror("access");
        return 1;
    }

    // 检查文件是否可写
    if (access(filename, W_OK) == 0) {
        printf("File is writable.\n");
    } else {
        perror("access");
        return 1;
    }

    // 检查文件是否可执行
    if (access(filename, X_OK) == 0) {
        printf("File is executable.\n");
    } else {
        perror("access");
        return 1;
    }

    return 0;
}

在这个示例中,程序使用 access 函数检查文件是否存在、是否可读、是否可写以及是否可执行,并根据检查结果输出相应的信息。


有名管道(Named Pipe),也称为FIFO(First In, First Out),是一种特殊的文件,它允许不具有亲缘关系的进程之间进行通信。与无名管道(匿名管道)不同,有名管道在文件系统中有一个特定的路径名,因此可以通过这个路径名被任何进程访问,只要它们有相应的权限。

有名管道的主要特点如下:

文件系统中的实体:有名管道在文件系统中以普通文件的形式存在,但它不占用磁盘空间,直到有数据写入。

半双工通信:与无名管道一样,有名管道也是半双工的,数据只能在一个方向上流动。一个进程向有名管道写入数据,另一个进程从管道读取数据。

阻塞特性:当没有进程在读取时,向有名管道写入数据的进程会被阻塞。同样,如果没有进程在写入,读取有名管道的进程也会被阻塞。

同步性:有名管道提供了同步机制,确保数据的顺序性。数据按照写入的顺序被读取。

创建和使用:有名管道通常通过 mkfifo() 系统调用创建,然后可以使用标准的文件操作函数(如 open(), read(), write(), close())进行操作。

权限控制:有名管道的创建者可以设置文件权限,以控制哪些用户或进程可以访问管道。

mkfifo()函数

mkfifo() 是一个在 Unix 和类 Unix 系统中用于创建有名管道(FIFO)的系统调用。有名管道是一种特殊的文件,它允许不具有亲缘关系的进程之间进行通信。这种文件在文件系统中有一个特定的路径名,但它不占用实际的磁盘空间,直到有数据被写入。

函数原型如下:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

参数说明:

  • pathname:指向一个字符串的指针,指定了要创建的 FIFO 文件的路径名。
  • mode:一个 mode_t 类型的值,用于设置新创建的 FIFO 文件的权限。这个模式会与当前进程的文件模式掩码(umask)进行按位与操作,以确定最终的文件权限。

返回值:

  • 成功时,返回 0。
  • 失败时,返回 -1,并设置 errno 以指示错误类型。

使用 mkfifo() 创建的 FIFO 文件具有以下特性:

  • 它遵循先进先出(FIFO)的原则,即数据按照写入的顺序被读取。
  • FIFO 是半双工的,这意味着在任何给定时间,只能有一个进程写入数据,另一个进程读取数据。
  • 如果没有进程在读取,写入 FIFO 的进程会被阻塞。同样,如果没有进程在写入,读取 FIFO 的进程也会被阻塞。

写端

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
    // 创建有名管道
    int ret = access("my_fifo",F_OK);
    if(ret == -1) {
        printf("管道不存在,创建管道");
        ret = mkfifo("my_fifo",0666);
        if(ret == -1) {
            perror("mkfifo");
            exit(EXIT_FAILURE);
        }
    }

    // 使用有名管道
    int fd = open("my_fifo", O_WRONLY); // 打开管道进行写入
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 写入数据
    const char *message = "Hello, FIFO!";
    write(fd, message, strlen(message));

    // 关闭文件描述符
    close(fd);

    return 0;
}

读端

//
// Created by ubuntu on 24-2-8.
//
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdbool.h>
int main() {
    int fd = open("my_fifo",O_RDONLY);
    if(fd == -1) {
        perror("open");
        exit(0);
    }
    while(true) {
        char buf[1024] = {0};
        int len = read(fd,buf,sizeof(buf));
        if(len == 0) {
            printf("写端断开连接了...\n");
            break;
        }
        printf("recv data: %s\n",buf);
    }
    close(fd);
    return 0;
}


//
// Created by ubuntu on 24-2-8.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

int main() {
    int ret = access("test_fifo", F_OK);
    if (ret == -1) {
        printf("不存在管道文件,创建管道文件\n");
        ret = mkfifo("test_fifo", 0666);
        if (ret == -1) {
            perror("mkfifo");
            exit(EXIT_FAILURE);
        }
    }
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        int fd = open("test_fifo", O_RDONLY);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }
        char buffer[1024];
        int len = read(fd, buffer, sizeof(buffer));
        if (len == 0) {
            printf("写端断开连接了...\n");
        }
        printf("recv data: %s\n", buffer);
        close(fd);
    } else {

        int fd = open("test_fifo", O_WRONLY);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }
        char buffer[1024] = {0};
        char *str = "helloworld";
        strcpy(buffer, str);
        write(fd, buffer, sizeof(buffer));
        close(fd);
    }
    return 0;
}

内存映射

mmap函数非常灵活,下面是对每个参数更详细的解释:
void *addr: 这是我们想要映射的内存地址的起始点。通常,我们为这个参数传入NULL,这允许系统选择一个合适的地址进行映射。
size_t length: 这个参数代表我们想要映射的内存的大小。
int prot: 这个参数用于设置映射区域的保护方式,它可以是以下几个常量的位或(bitwise or)组合:
PROT_EXEC 映射区域页可执行
PROT_READ 映射区域页可读
PROT_WRITE 映射区域页可写
PROT_NONE 映射区域页不可访问
int flags: 该参数决定了映射对象的采取的其他操作,比如:
MAP_SHARED:对该映射的修改将直接反映在其所参照的文件上,并且对其他正在映射同一文件的进程可见。
MAP_PRIVATE:对该映射的修改不会写回其参照的文件,而且对其他进程不可见,只在当前进程中有效。
MAP_ANONYMOUS:创建的映射不与任何文件相关联,主要用于进程间通信、动态内存分配等等。
int fd: 为了进行映射,我们需要一个文件描述符来参照物,这就是这个参数的目的。它是一个打开的文件描述符。
off_t offset: 从文件的什么位置开始映射。

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    // 创建共享内存
    /*
     * 在mmap()函数中,flags参数用于指定内存映射的属性,其中:

- `MAP_SHARED` 需要单独和其他标志一起使用,表示创建的是一个共享内存区域。如果多个进程都映射到了一块这样的内存,那么任何一个进程对这块内存的改变都会立即反映在其他所有进程中看到的这块内存上。

- `MAP_ANONYMOUS` 指定使用匿名映射,也就是说映射区不与任何文件关联。通常和 `MAP_SHARED` 一起使用,用来在多个不相关的进程之间共享内存。

所以,在你提出的这段代码中, `MAP_SHARED | MAP_ANONYMOUS` 表示创建的是一个可以在多个进程间共享的匿名内存映射区域。此内存映射区在进程间共享,且不与任何文件关联。也就是说,这段代码可以创建一块内存区域,多个进程可以共享这块内存区域,以实现进程间的通信。
     * */
    int *shared_memory = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

    *shared_memory = 0;

    // 创建子进程
    pid_t pid = fork();

    if (pid < 0) {
        printf("Fork failed\n");
        return 1;
    }

    if (pid == 0) {
        printf("In child process\n");
        (*shared_memory)++;
        printf("Child changed shared_memory to %d\n", *shared_memory);
    } else {
        printf("In parent process\n");
        wait(NULL);
        printf("Parent sees shared_memory as %d\n", *shared_memory);
    }

    return 0;
}


在C语言中,下面的代码段就是一个使用文件描述符和mmap修改文件内容的例子:

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>

int main() {
    /* 文件路径 */
    char *filepath = "example.txt";
    /* 需要写入的内容 */
    char *message = "Hello, mmap!";
    
    /* 打开文件,并获取文件描述符 */
    int fd = open(filepath, O_RDWR);
    if (fd < 0) {
        perror("Error opening file for writing");
        return -1;
    }

    /* 获取映射区域 */
    char *map = mmap(0, strlen(message), PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) {
        perror("Error mapping file");
        return -1;
    }

    /* 将需要写入的数据复制到映射区域 */
    memcpy(map, message, strlen(message));

    /* 清除映射 */
    munmap(map, strlen(message));

    /* 关闭文件 */
    close(fd);

    return 0;
}

该程序会打开一个名为"example.txt"的文件,然后创建一个映射区并将"Hello, mmap!"写入到文件中,最后清除映射并关闭文件。

这就是一个使用mmap()函数和文件描述符(fd)修改文件内容的简单例子。当然,实际的使用场景可能会更复杂,更需要注意错误处理和资源清理。