进程间的通信

一,无名管道

在 C 语言中,无名管道(也称为匿名管道)是一种进程间通信的方式,它主要用于具有亲缘关系的进程之间(如父子进程)进行单向数据传输。

无名管道的简介

Linux 下的进程通信手段基本上是从 Unix 平台上继承而来的
每个进程都有自己独立的地址空间,当两个不同进程需要进行数据交互时,就需要使用进程间通讯
进程间通讯分为单个计算机的进程间通讯和局域网的进程间通讯
进程间通讯方式有 管道,信号,消息队列,共享内存,网络
管道的本质是在内存建立一段缓冲区,由操作系统内核来负责创建与管理。具体通讯模型如下 :

一、无名管道的特点

  1. 单向通信:无名管道只能在一个方向上传输数据,即从一个进程的写入端(write end)流向另一个进程的读取端(read end)。

  1. 亲缘关系限制:通常只能在具有亲缘关系的进程之间使用,例如父子进程。这是因为无名管道是在创建进程时通过继承关系传递的。
  2. 基于文件描述符:无名管道在操作系统内部被实现为文件描述符。写入端和读取端分别对应不同的文件描述符。
  3. 数据传输方式:数据以字节流的形式在管道中传输,没有消息边界。读取进程可能需要根据特定的格式或协议来解析数据。

二、创建无名管道

在 C 语言中,可以使用pipe函数来创建无名管道。

函数原型:int pipe(int pipefd[2]);

参数说明:

  • pipefd是一个包含两个整数的数组。pipefd[0]表示管道的读取端文件描述符,pipefd[1]表示管道的写入端文件描述符。

返回值:如果成功创建管道,函数返回 0;如果失败,返回 -1,并设置errno来指示错误原因。

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

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    // 这里可以进行父子进程的创建和数据传输操作

    close(pipefd[0]);
    close(pipefd[1]);
    return 0;
}

三、使用无名管道进行进程间通信

  1. 父子进程通信示例:

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

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        close(pipefd[1]); // 关闭写入端
        char buffer[100];
        ssize_t bytesRead = read(pipefd[0], buffer, sizeof(buffer));
        if (bytesRead == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        buffer[bytesRead] = '\0';
        printf("Child received: %s\n", buffer);
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        close(pipefd[0]); // 关闭读取端
        const char *message = "Hello from parent!";
        ssize_t bytesWritten = write(pipefd[1], message, strlen(message));
        if (bytesWritten == -1) {
            perror("write");
            return 1;
        }
        close(pipefd[1]);
        wait(NULL);
    }
    return 0;
}

在这个例子中,父进程向管道写入数据,子进程从管道读取数据,实现了父子进程之间的单向通信。

  1. 注意事项:
    • 无名管道的容量是有限的,如果写入进程写入数据的速度超过读取进程读取数据的速度,管道可能会被填满,导致写入进程阻塞。
    • 读取进程在管道中没有数据可读时也会阻塞,直到有数据可用或者管道被关闭。
    • 在使用完管道后,应该及时关闭管道的读取端和写入端文件描述符,以避免资源泄漏和出现不可预测的行为。
    • . 当管道的写端被关闭 ,从管道中读取剩余数据后, read 函数返回 0
    • 在写入管道时,确保不超过 PIPE_BUF 字节的操作是原子的
      关于 PIPE_BUF 原子操作,可以通过 man 7 pipe, 搜索 PIPE_BUF ,则可以看到相关解释

                当写入的数据达到PIPE_BUF字节时,write()会在必要的时候阻塞直到管道中的可用空间足以 原子地
完成操作

无名管道是一种简单而有效的进程间通信方式,但由于其局限性(如只能在亲缘关系的进程之间使用、单向通信等),在一些复杂的应用场景中可能需要结合其他进程间通信机制一起使用。

二,有名管道

有名管道(也称为命名管道)是一种进程间通信的方式,它可以在不具有亲缘关系的进程之间进行数据传输。

有名管道简介

有名管道是 文件系统中可见的文件,但是不占用磁盘空间,仍然在内存中。可以通过 mkfifo 命令创建有 名管道 (在共享目录不能使用 mkfifo)

有名管道与无名管道一样,在应用层是基于文件接口进行操作
有名管道用于 任意进程之间的通讯 ,当管道为空时 , 读进程会阻塞。

一、有名管道的特点

  1. 命名标识:有名管道有一个特定的文件名,可以在文件系统中被不同的进程访问。这使得不具有亲缘关系的进程可以通过这个文件名来打开管道进行通信。
  2. 双向通信(可模拟):虽然有名管道本身是单向的,但可以通过创建两个有名管道来实现双向通信,一个用于从进程 A 向进程 B 传输数据,另一个用于从进程 B 向进程 A 传输数据。
  3. 可持久化:有名管道的存在不依赖于创建它的进程,只要不被删除,它会一直存在于文件系统中。
  4. 基于文件操作:对有名管道的操作类似于对文件的操作,可以使用文件描述符进行读写。

二、创建有名管道

在 C 语言中,可以使用mkfifo函数或mknod系统调用来创建有名管道。

  1. mkfifo函数:

    • 函数原型:int mkfifo(const char *pathname, mode_t mode);
    • 参数说明:
      • pathname是要创建的有名管道的文件名。
      • mode是管道的权限模式,类似于文件的权限设置,例如0666表示读写权限对所有用户开放。
    • 返回值:如果成功创建管道,函数返回 0;如果失败,返回 -1,并设置errno来指示错误原因。
  2. mknod系统调用:

    • 函数原型:int mknod(const char *pathname, mode_t mode, dev_t dev);
    • 参数说明:
      • pathname是要创建的有名管道的文件名。
      • mode是文件的类型和权限模式,对于有名管道,文件类型应该设置为S_IFIFO(表示有名管道类型)。
      • dev参数通常可以设置为 0。
    • 返回值:如果成功创建管道,函数返回 0;如果失败,返回 -1,并设置errno来指示错误原因。

例如:

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

int main() {
    const char *fifoPath = "myfifo";
    if (mkfifo(fifoPath, 0666) == -1) {
        perror("mkfifo");
        return 1;
    }
    // 这里可以进行有名管道的使用操作
    unlink(fifoPath); // 删除有名管道文件(可选)
    return 0;
}

三、使用有名管道进行进程间通信

  1. 单向通信示例:

// writer.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *fifoPath = "myfifo";
    int fd = open(fifoPath, O_WRONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    const char *message = "Hello from writer!";
    ssize_t bytesWritten = write(fd, message, strlen(message));
    if (bytesWritten == -1) {
        perror("write");
        return 1;
    }
    close(fd);
    return 0;
}

// reader.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    const char *fifoPath = "myfifo";
    int fd = open(fifoPath, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    char buffer[100];
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        perror("read");
        return 1;
    }
    buffer[bytesRead] = '\0';
    printf("Reader received: %s\n", buffer);
    close(fd);
    return 0;
}

在这个例子中,writer.c进程向有名管道写入数据,reader.c进程从有名管道读取数据。

  1. 双向通信示例:

// process1.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *fifo1Path = "fifo1";
    const char *fifo2Path = "fifo2";
    int fd1 = open(fifo1Path, O_WRONLY);
    int fd2 = open(fifo2Path, O_RDONLY);
    if (fd1 == -1 || fd2 == -1) {
        perror("open");
        return 1;
    }
    const char *message = "Hello from process1!";
    ssize_t bytesWritten = write(fd1, message, strlen(message));
    if (bytesWritten == -1) {
        perror("write");
        return 1;
    }
    char buffer[100];
    ssize_t bytesRead = read(fd2, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        perror("read");
        return 1;
    }
    buffer[bytesRead] = '\0';
    printf("Process1 received: %s\n", buffer);
    close(fd1);
    close(fd2);
    return 0;
}

// process2.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *fifo1Path = "fifo1";
    const char *fifo2Path = "fifo2";
    int fd1 = open(fifo1Path, O_RDONLY);
    int fd2 = open(fifo2Path, O_WRONLY);
    if (fd1 == -1 || fd2 == -1) {
        perror("open");
        return 1;
    }
    char buffer[100];
    ssize_t bytesRead = read(fd1, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        perror("read");
        return 1;
    }
    buffer[bytesRead] = '\0';
    printf("Process2 received: %s\n", buffer);
    const char *reply = "Hello from process2!";
    ssize_t bytesWritten = write(fd2, reply, strlen(reply));
    if (bytesWritten == -1) {
        perror("write");
        return 1;
    }
    close(fd1);
    close(fd2);
    return 0;
}

在这个例子中,通过创建两个有名管道fifo1fifo2,实现了两个进程之间的双向通信。

四、注意事项

  1. 权限设置:在创建有名管道时,要确保设置合适的权限,以便其他进程能够访问管道。如果权限设置不当,可能会导致其他进程无法打开管道进行通信。
  2. 打开方式:在打开有名管道时,要根据需要选择正确的打开方式(只读、只写或读写)。如果多个进程同时以不兼容的方式打开管道,可能会导致阻塞或错误。
  3. 数据同步:由于有名管道是单向的,并且数据的读取和写入可能在不同的进程中进行,需要考虑数据的同步问题。例如,可以使用信号量或其他同步机制来确保数据的正确传输和处理。
  4. 管道清理:在不再需要有名管道时,应该及时删除管道文件,以释放系统资源。可以使用unlink函数来删除有名管道文件。

三,信号

在 C 语言中,信号是一种进程间通信的机制,可以用于在不同进程之间传递异步事件通知。以下是关于 C 语言中进程间通信信号的详细介绍:

一、信号的概念

  1. 信号的定义:

    • 信号是一种软件中断,它由操作系统发送给一个或多个进程,以通知发生了特定的事件。
    • 这些事件可以是硬件异常(如除零错误、内存访问错误等)、软件事件(如用户按下 Ctrl+C 中断程序、定时器到期等)或其他系统事件。
  2. 信号的种类:

    • 不同的操作系统通常支持多种不同的信号。例如,在 Unix/Linux 系统中,常见的信号有SIGINT(中断信号,通常由用户按下 Ctrl+C 产生)、SIGTERM(终止信号,可以由其他进程发送来请求终止目标进程)、SIGKILL(强制终止信号,无法被捕获或忽略)等。
    • Linux 系统可以通过 kill -l 命令查看,常用的信号列举如下
  3. .信号的来源
                在 Linux 中信号一般的来源如下:
                程序执行错误,如内存访问越界,数学运算除0
                由其他进程发送
                通过控制终端发送,如ctrl + c
                子进程结束时向父进程发送的SIGCHLD信号
                程序中设定的定时器产生的SIGALRM信号

二、信号的发送和接收

  1. 发送信号:
    • 一个进程可以使用kill函数向另一个进程发送信号。
    • 函数原型:int kill(pid_t pid, int sig);
    • 参数说明:
      • pid是目标进程的进程 ID。如果pid为正数,表示发送信号给特定的进程;如果pid为 0,表示发送信号给与调用进程同组的所有进程;如果pid为 -1,表示发送信号给系统中的所有进程。
      • sig是要发送的信号编号。
    • 例如:

     #include <stdio.h>
     #include <signal.h>
     #include <sys/types.h>
     #include <unistd.h>

     int main() {
         pid_t target_pid;
         printf("Enter the process ID to send a signal to: ");
         scanf("%d", &target_pid);
         if (kill(target_pid, SIGINT) == -1) {
             perror("kill");
             return 1;
         }
         printf("Signal sent.\n");
         return 0;
     }

        在 C 语言中,alarm函数用于设置一个定时器,当定时器到期时,会向当前进程发送SIGALRM信号。

函数原型:unsigned int alarm(unsigned int seconds);

参数说明:

  • seconds:指定定时器的时间(以秒为单位)。当这个时间过去后,会产生SIGALRM信号。

返回值

返回值是上一个定时器剩余的时间,如果之前没有设置过定时器,则返回 0。

使用示例

以下是一个使用alarm函数的示例:

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

void signalHandler(int signum) {
    if (signum == SIGALRM) {
        printf("Alarm triggered!\n");
    }
}

int main() {
    signal(SIGALRM, signalHandler);
    alarm(5);
    printf("Waiting for alarm...\n");
    pause();
    return 0;
}

在这个例子中,首先设置了一个信号处理函数来处理SIGALRM信号。然后调用alarm(5)设置一个 5 秒的定时器。接着,程序进入pause等待,直到有信号到来。当 5 秒过去后,定时器到期,会触发SIGALRM信号,信号处理函数被调用,输出 “Alarm triggered!”。

  1. 接收信号:
    • 进程可以通过注册信号处理函数来接收信号。
    • 可以使用signal函数或sigaction函数来设置信号处理函数。
    • signal函数原型:void (*signal(int sig, void (*handler)(int)))(int);
    • 参数说明:
      • sig是要处理的信号编号。
      • handler是信号处理函数的指针,当指定的信号发生时,该函数将被调用。
    • sigaction函数提供了更强大的信号处理功能,它可以设置更详细的信号处理选项。
    • 例如:

     #include <stdio.h>
     #include <signal.h>

     void signalHandler(int signum) {
         printf("Received signal %d\n", signum);
     }

     int main() {
         signal(SIGINT, signalHandler);
         while (1) {
             // 程序的主要逻辑
         }
         return 0;
     }

三、信号的作用和应用场景

  1. 中断和终止程序:

    • 用户可以通过发送SIGINT信号来中断正在运行的程序。例如,在命令行中按下 Ctrl+C 会发送SIGINT信号给前台进程。
    • 其他进程也可以发送SIGTERMSIGKILL信号来请求终止目标进程。
  2. 同步和协调:

    • 信号可以用于在不同进程之间进行同步和协调。例如,一个进程可以发送信号来通知另一个进程某个事件已经发生,或者请求另一个进程执行特定的任务。
  3. 错误处理:

    • 当发生硬件异常或其他错误情况时,操作系统会发送相应的信号给进程。进程可以通过捕获这些信号来进行错误处理,例如清理资源、记录错误信息等。

四、注意事项

  1. 信号的可靠性:

    • 信号是一种异步通信机制,因此在处理信号时需要注意信号的可靠性。由于信号可能在任何时候发生,并且可能会打断正在执行的代码,因此在信号处理函数中应该尽量避免执行复杂的操作,以免引起不可预测的结果。
  2. 信号的竞争条件:

    • 在多个进程同时发送和接收信号时,可能会出现竞争条件。例如,如果两个进程同时发送信号给同一个进程,并且该进程的信号处理函数正在执行中,那么可能会导致信号丢失或处理顺序不确定的问题。为了避免竞争条件,可以使用信号量或其他同步机制来协调信号的发送和接收。
  3. 可重入性:

    • 信号处理函数应该是可重入的,即它们可以在任何时候被调用,并且不会破坏程序的状态。在信号处理函数中,应该避免使用全局变量和静态变量,以免引起数据不一致的问题。

总之,信号是 C 语言中一种重要的进程间通信机制,它可以用于在不同进程之间传递异步事件通知。在使用信号时,需要注意信号的可靠性、竞争条件和可重入性等问题,以确保程序的正确性和稳定性。

四,消息队列

在 C 语言中,消息队列是一种进程间通信(IPC)机制,它允许不同的进程之间以异步的方式传递消息。

简介

        IPC : Inter-Process Communication ( 进程间通讯 )
        System V IPC 对象共有三种
                消息队列
                共享内存
                信号量

一、消息队列的特点

  1. 异步通信:发送和接收消息的进程不需要同时运行。发送进程可以将消息放入队列中,然后继续执行其他任务,接收进程可以在合适的时候从队列中取出消息进行处理。
  2. 有类型的消息:消息队列中的消息可以有不同的类型,接收进程可以根据消息类型选择接收特定类型的消息。
  3. 先进先出(FIFO)顺序:消息按照放入队列的顺序依次被接收,保证了消息的顺序性。
  4. 可持久化:消息队列可以在系统重启后仍然保留消息,除非显式地删除。
  5. 消息队列是属于 Sytem V IPC 的一种,由 内核维护与管理 ,通过 ipcs -q 查看

二、使用消息队列进行进程间通信的步骤

  1. 创建或打开消息队列:

    • 使用msgget函数创建一个新的消息队列或打开一个已存在的消息队列。
    • 函数原型:int msgget(key_t key, int msgflg);
    • 参数说明:
      • key是一个键值,用于唯一标识一个消息队列。可以使用ftok函数根据一个存在的文件路径和一个整数生成一个唯一的键值。
      • msgflg是标志位,用于指定创建或打开消息队列的选项,例如权限设置等。
    • 返回值:成功时返回消息队列的标识符,失败时返回 -1。
  2. 发送消息:

    • 使用msgsnd函数将消息发送到消息队列中。
    • 函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    • 参数说明:
      • msqid是消息队列的标识符。
      • msgp是指向要发送的消息结构体的指针。消息结构体通常包含消息类型和消息内容。
      • msgsz是消息内容的大小。
      • msgflg是标志位,用于指定发送消息的选项,例如是否阻塞等。
    • 返回值:成功时返回 0,失败时返回 -1。
  3. 接收消息:

    • 使用msgrcv函数从消息队列中接收消息。
    • 函数原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    • 参数说明:
      • msqid是消息队列的标识符。
      • msgp是指向接收消息的缓冲区的指针。
      • msgsz是接收消息的缓冲区大小。
      • msgtyp是指定要接收的消息类型。可以是特定的消息类型,或者使用特殊的值来接收任何类型的消息。
      • msgflg是标志位,用于指定接收消息的选项,例如是否阻塞等。
    • 返回值:成功时返回接收到的消息的实际大小,失败时返回 -1。
  4. 删除消息队列:

    • 使用msgctl函数可以对消息队列进行各种控制操作,包括删除消息队列。
    • 函数原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    • 参数说明:
      • msqid是消息队列的标识符。
      • cmd是控制命令,例如IPC_RMID用于删除消息队列。
      • buf是一个指向struct msqid_ds结构体的指针,用于存储或修改消息队列的属性信息。在删除消息队列时,可以将其设置为NULL
    • 返回值:成功时返回 0,失败时返回 -1。

三、示例代码

以下是一个使用消息队列进行进程间通信的示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

// 定义消息结构体
struct message {
    long mtype;
    char mtext[100];
};

int main() {
    key_t key;
    int msqid;
    struct message msg;

    // 使用 ftok 生成唯一的键值
    key = ftok(".", 'm');

    // 创建消息队列
    msqid = msgget(key, 0666 | IPC_CREAT);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    // 发送消息
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello from sender!");
    if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
        perror("msgsnd");
        exit(1);
    }

    // 接收消息
    if (msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
        perror("msgrcv");
        exit(1);
    }
    printf("Received message: %s\n", msg.mtext);

    // 删除消息队列
    if (msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        exit(1);
    }

    return 0;
}

在这个例子中,首先生成一个唯一的键值,然后创建一个消息队列。发送进程将一条消息放入队列中,接收进程从队列中取出消息并打印。最后,删除消息队列。

四、注意事项

  1. 消息队列的容量限制:消息队列有一定的容量限制,如果发送的消息过多而接收不及时,可能会导致队列满,发送进程可能会被阻塞或返回错误。
  2. 消息大小限制:消息的大小也有一定的限制,通常不能超过系统规定的最大值。在设计消息结构体时要考虑到这个限制。
  3. 错误处理:在使用消息队列的函数时,要检查返回值并进行适当的错误处理,以确保程序的稳定性。
  4. 同步问题:如果多个进程同时访问消息队列,可能需要考虑同步问题,以避免竞争条件和数据不一致。可以使用信号量等机制来实现同步。

。五,共享内存

在 C 语言中,共享内存是一种进程间通信(IPC)机制,它允许不同的进程访问同一块物理内存区域,从而实现高效的数据共享。

简介

        共享内存是将分配的物理空间直接映射到进程的用户虚拟地址空间中,减少数据在内核空间缓存
        共享内存是一种效率较高的进程间通讯的方式
        在 Linux 系统中通过 ipcs -m 查看所有的共享内存
共享内存模型图

一、共享内存的特点

  1. 高效性:由于多个进程可以直接访问同一块内存,避免了数据的复制和传递,因此共享内存通常是最快的 IPC 机制之一。
  2. 灵活性:可以在不同的进程之间共享各种类型的数据,包括结构体、数组等。
  3. 同步问题:多个进程同时访问共享内存时,需要进行同步控制,以避免数据冲突和不一致性。

二、使用共享内存进行进程间通信的步骤

  1. 创建或打开共享内存:

    • 使用shmget函数创建一个新的共享内存段或打开一个已存在的共享内存段。
    • 函数原型:int shmget(key_t key, size_t size, int shmflg);
    • 参数说明:
      • key是一个键值,用于唯一标识一个共享内存段。可以使用ftok函数根据一个存在的文件路径和一个整数生成一个唯一的键值。
      • size是共享内存段的大小。
      • shmflg是标志位,用于指定创建或打开共享内存段的选项,例如权限设置等。
    • 返回值:成功时返回共享内存段的标识符,失败时返回 -1。
  2. 映射共享内存到进程地址空间:

    • 使用shmat函数将共享内存段映射到调用进程的地址空间。
    • 函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
    • 参数说明:
      • shmid是共享内存段的标识符。
      • shmaddr是指定映射的地址,可以为 NULL,表示由系统自动选择地址。
      • shmflg是标志位,用于指定映射的选项,例如是否可读可写等。
    • 返回值:成功时返回映射后的地址,失败时返回 (void *)-1。
  3. 使用共享内存:

    • 一旦共享内存被映射到进程地址空间,就可以像使用普通内存一样进行读写操作。
    • 可以在共享内存中存储各种数据结构,并通过指针进行访问和修改。
  4. 分离共享内存:

    • 使用shmdt函数将共享内存段从调用进程的地址空间分离。
    • 函数原型:int shmdt(const void *shmaddr);
    • 参数说明:shmaddr是映射后的地址,由shmat函数返回。
    • 返回值:成功时返回 0,失败时返回 -1。
  5. 删除共享内存:

    • 使用shmctl函数可以对共享内存段进行各种控制操作,包括删除共享内存段。
    • 函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    • 参数说明:
      • shmid是共享内存段的标识符。
      • cmd是控制命令,例如IPC_RMID用于删除共享内存段。
      • buf是一个指向struct shmid_ds结构体的指针,用于存储或修改共享内存段的属性信息。在删除共享内存段时,可以将其设置为NULL
    • 返回值:成功时返回 0,失败时返回 -1。

三、示例代码

以下是一个使用共享内存进行进程间通信的示例:

另一个进程读取共享内存中的数据:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024

int main() {
    key_t key;
    int shmid;
    char *shmaddr;

    // 使用 ftok 生成唯一的键值
    key = ftok(".", 's');

    // 打开共享内存段
    shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 映射共享内存段到进程地址空间
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    // 读取共享内存中的数据
    printf("Received data: %s\n", shmaddr);

    // 分离共享内存段
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(1);
    }

    return 0;
}

四、注意事项

  1. 同步控制:由于多个进程可以同时访问共享内存,需要进行同步控制,以避免数据冲突。可以使用信号量、互斥锁等机制来实现同步。
  2. 错误处理:在使用共享内存的函数时,要检查返回值并进行适当的错误处理,以确保程序的稳定性。
  3. 内存泄漏:在不再需要共享内存时,要及时删除共享内存段,以避免内存泄漏。
  4. 可移植性:不同的操作系统对共享内存的实现可能会有所不同,所以在使用时要考虑到可移植性问题。

六,信号量

在 C 语言中,信号量(Semaphore)是一种用于进程间同步和互斥的机制。它可以用来控制对共享资源的访问,确保多个进程或线程能够以正确的顺序和方式访问这些资源,避免竞争条件和数据不一致的问题。

简介

        资源竞争 : 当多个进程同时访问共享资源时,会产生资源竞争, 最终最导致数据混乱
        临界资源 : 不允许同时有多个进程访问的资源,包括硬件资源 (CPU 、内存、存储器以及其他外围设 备) 与软件资源 ( 共享代码段、共享数据结构 )
        临界区 : 访问临界资源代码

一、信号量的概念

  1. 信号量的定义:

    • 信号量是一个整数变量,它与一组等待进程和一组可用资源相关联。
    • 信号量的值表示当前可用的资源数量。当一个进程需要访问共享资源时,它会尝试减少信号量的值。如果信号量的值大于零,进程可以继续执行;如果信号量的值为零,进程会被阻塞,直到有其他进程释放资源,使信号量的值大于零。
  2. 信号量的操作:

    • 信号量有两个主要操作:P操作(也称为wait操作)和V操作(也称为signal操作)。
    • P操作:用于减少信号量的值。如果信号量的值大于零,P操作会将信号量的值减一,并立即返回;如果信号量的值为零,P操作会阻塞当前进程,直到有其他进程执行V操作,使信号量的值大于零。
    • V操作:用于增加信号量的值。V操作会将信号量的值加一,并唤醒一个等待在该信号量上的进程(如果有)。

二、使用信号量进行进程间同步的步骤

  1. 包含头文件:

    • 在 C 语言中,使用信号量需要包含<semaphore.h>头文件。
  2. 初始化信号量:

    • 使用sem_init函数初始化一个信号量。
    • 函数原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
    • 参数说明:
      • sem是指向要初始化的信号量的指针。
      • pshared表示信号量是在进程间共享还是在同一进程的线程间共享。如果pshared为 0,表示信号量在线程间共享;如果pshared为非零值,表示信号量在进程间共享。
      • value是信号量的初始值。
    • 返回值:成功时返回 0,失败时返回 -1。
  3. 进行P操作和V操作:

    • 使用sem_wait函数进行P操作,使用sem_post函数进行V操作。
    • sem_wait函数原型:int sem_wait(sem_t *sem);
    • sem_post函数原型:int sem_post(sem_t *sem);
    • 参数说明:sem是指向要操作的信号量的指针。
    • 返回值:成功时返回 0,失败时返回 -1。
  4. 销毁信号量:

    • 使用sem_destroy函数销毁一个已经初始化的信号量。
    • 函数原型:int sem_destroy(sem_t *sem);
    • 参数说明:sem是指向要销毁的信号量的指针。
    • 返回值:成功时返回 0,失败时返回 -1。

三、示例代码

以下是一个使用信号量进行进程间同步的示例:

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

sem_t semaphore;

void process1() {
    printf("Process 1 is waiting for the semaphore.\n");
    sem_wait(&semaphore);
    printf("Process 1 acquired the semaphore.\n");
    // 模拟对共享资源的访问
    sleep(2);
    printf("Process 1 is releasing the semaphore.\n");
    sem_post(&semaphore);
}

void process2() {
    printf("Process 2 is waiting for the semaphore.\n");
    sem_wait(&semaphore);
    printf("Process 2 acquired the semaphore.\n");
    // 模拟对共享资源的访问
    sleep(2);
    printf("Process 2 is releasing the semaphore.\n");
    sem_post(&semaphore);
}

int main() {
    // 初始化信号量,初始值为 1,表示只有一个资源可用
    sem_init(&semaphore, 0, 1);

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // 子进程 1
        process1();
        exit(0);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // 子进程 2
        process2();
        exit(0);
    }

    // 等待子进程结束
    wait(NULL);
    wait(NULL);

    // 销毁信号量
    sem_destroy(&semaphore);

    return 0;
}

在这个例子中,两个进程(process1process2)通过信号量进行同步。信号量的初始值为 1,表示只有一个资源可用。每个进程在访问共享资源之前都会执行P操作(sem_wait),如果信号量的值为零,进程会被阻塞。当一个进程访问完共享资源后,会执行V操作(sem_post),将信号量的值加一,唤醒一个等待在该信号量上的进程。

四、注意事项

  1. 错误处理:在使用信号量的函数时,要检查返回值并进行适当的错误处理,以确保程序的稳定性。
  2. 死锁问题:如果多个进程或线程在获取信号量时的顺序不当,可能会导致死锁。在设计程序时,要注意避免死锁的发生。
  3. 可移植性:不同的操作系统对信号量的实现可能会有所不同,所以在使用时要考虑到可移植性问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值