Linux进程间通信的方式汇总

Linux进程间通信的方式汇总

Linux进程间通信的方式主要有以下几种:

  1. 管道(Pipe):管道是一种最简单的IPC方式,用于实现父子进程之间的通信。一个进程可以向管道中写入数据,另一个进程可以从管道中读取数据。
  2. 命名管道(Named Pipe)或FIFO(First In First Out):类似于管道,但是命名管道允许无亲缘关系的进程之间进行通信。它在文件系统中有对应的文件名,并通过文件描述符来访问。
  3. 信号(Signal):信号是在软件层次上对中断机制的一种模拟,用于通知进程有某事件发生。一个进程收到一个信号后,可以根据信号的不同含义来执行不同的操作。
  4. 消息队列(Message Queue):消息队列是消息的链接表,它允许进程之间通过发送和接收消息来进行通信。消息队列被内核维护,保证了消息的独立性和安全性。
  5. 共享内存(Shared Memory):共享内存允许多个进程访问同一块内存区域,进程可以直接读写该内存区域来进行通信。这种方式通信速度快,但需要同步机制来避免冲突。
  6. 信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制,可以控制对共享资源的访问,避免竞争和死锁等问题。
  7. 套接字(Socket):套接字不仅可以用于本地进程间通信,还可以用于网络通信。它提供了一种跨网络平台的进程间通信机制,使得不同主机上的进程可以进行通信。

这些通信方式各有特点,可以根据具体的应用场景和需求来选择合适的通信方式。

管道

下面是一个简单的 Linux C 示例代码,它演示了如何使用管道(pipe)来实现父进程和子进程之间的通信。在这个例子中,父进程会向管道中写入一个字符串,然后子进程会从管道中读取这个字符串并打印出来。

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/wait.h>  
  
int main() {  
    int fd[2]; // 文件描述符数组,用于管道的读写  
    pid_t pid; // 子进程的进程ID  
    char buf[1024]; // 用于存储从管道读取的数据的缓冲区  
    const char *message = "Hello from parent process!"; // 父进程要发送的消息  
  
    // 创建管道  
    if (pipe(fd) == -1) {  
        perror("pipe");  
        exit(EXIT_FAILURE);  
    }  
  
    // 创建子进程  
    pid = fork();  
    if (pid == -1) {  
        perror("fork");  
        exit(EXIT_FAILURE);  
    }  
  
    // 父进程  
    if (pid > 0) {  
        close(fd[0]); // 关闭读端,因为父进程要写入  
  
        // 写入消息到管道  
        write(fd[1], message, strlen(message) + 1);  
        close(fd[1]); // 写入完毕后关闭写端  
  
        // 等待子进程结束  
        waitpid(pid, NULL, 0);  
        printf("Parent process has finished.\n");  
    }  
    // 子进程  
    else {  
        close(fd[1]); // 关闭写端,因为子进程要读取  
  
        // 从管道读取消息  
        ssize_t bytesRead = read(fd[0], buf, sizeof(buf) - 1);  
        if (bytesRead > 0) {  
            buf[bytesRead] = '\0'; // 在读取到的字符串末尾添加空字符  
            printf("Child process received: %s\n", buf);  
        }  
        close(fd[0]); // 读取完毕后关闭读端  
  
        exit(EXIT_SUCCESS);  
    }  
  
    return 0;  
}

在这个示例中,首先通过 pipe() 函数创建了一个管道,并返回了两个文件描述符 fd[0]fd[1],分别代表管道的读端和写端。

接着,使用 fork() 函数创建了一个子进程。在父进程中,关闭了管道的读端,并使用 write() 函数将消息写入管道的写端。写入完成后,关闭了写端,并等待子进程结束。

在子进程中,关闭了管道的写端,并使用 read() 函数从管道的读端读取消息。读取到的消息存储在 buf 缓冲区中,并打印出来。读取完成后,关闭了读端,并退出子进程。

注意,在实际使用中,通常需要对 read()write() 函数的返回值进行检查,以处理可能出现的错误情况。在这个简单的示例中,为了简洁,我省略了错误检查代码。在实际编程中,请确保添加适当的错误处理逻辑。

命名管道

下面是一个简单的 Linux C 示例代码,它演示了如何使用命名管道(named pipe)来实现进程间的通信。在这个例子中,我们将创建一个命名管道,一个进程写入数据,另一个进程读取数据。

首先是写入数据的进程(writer.c):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
#include <sys/types.h>  
#include <unistd.h>  
  
#define FIFO_PATH "/tmp/my_named_pipe"  
#define BUFFER_SIZE 256  
  
int main() {  
    int fd;  
    char buffer[BUFFER_SIZE];  
    const char *message = "Hello from named pipe writer!";  
  
    // 创建命名管道  
    if (mkfifo(FIFO_PATH, 0666) == -1) {  
        perror("mkfifo");  
        exit(EXIT_FAILURE);  
    }  
  
    // 打开命名管道以写入数据  
    fd = open(FIFO_PATH, O_WRONLY);  
    if (fd == -1) {  
        perror("open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 写入数据到命名管道  
    if (write(fd, message, strlen(message) + 1) == -1) {  
        perror("write");  
        exit(EXIT_FAILURE);  
    }  
  
    close(fd);  
    printf("Writer process has finished writing.\n");  
  
    // 删除命名管道  
    unlink(FIFO_PATH);  
  
    return 0;  
}

然后是读取数据的进程(reader.c):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <fcntl.h>  
#include <unistd.h>  
  
#define FIFO_PATH "/tmp/my_named_pipe"  
#define BUFFER_SIZE 256  
  
int main() {  
    int fd;  
    char buffer[BUFFER_SIZE];  
  
    // 打开命名管道以读取数据  
    fd = open(FIFO_PATH, O_RDONLY);  
    if (fd == -1) {  
        perror("open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 从命名管道读取数据  
    ssize_t bytesRead = read(fd, buffer, BUFFER_SIZE - 1);  
    if (bytesRead == -1) {  
        perror("read");  
        exit(EXIT_FAILURE);  
    }  
  
    buffer[bytesRead] = '\0'; // 添加字符串结束符  
    printf("Reader process received: %s\n", buffer);  
  
    close(fd);  
  
    return 0;  
}

要运行这两个程序,你需要分别编译它们:

gcc -o writer writer.c  
gcc -o reader reader.c

然后,你可以先运行 reader 进程来等待命名管道的打开:

./reader

在另一个终端窗口中,运行 writer 进程来写入数据:

./writer

如果一切正常,你应该在第一个终端窗口中看到 reader 进程打印出从 writer 进程接收到的消息。

注意,命名管道(FIFO)在文件系统中有一个路径,所以你需要确保该路径是存在的并且有足够的权限进行读写操作。此外,你还需要处理可能发生的错误情况,比如命名管道不存在或打开失败等。在这个简单的示例中,为了保持代码的简洁性,我省略了这些错误处理逻辑。在实际编程中,请确保添加适当的错误处理代码。

信号

下面是一个简单的 Linux C 示例代码,它演示了如何使用信号(Signal)来在进程之间发送通知。在这个例子中,我们将创建一个进程,它将发送一个 SIGUSR1 信号给自身,并在接收到信号时执行一个特定的处理函数。

#include <stdio.h>  
#include <stdlib.h>  
#include <signal.h>  
#include <unistd.h>  
  
// 信号处理函数  
void signal_handler(int signo) {  
    if (signo == SIGUSR1) {  
        printf("Received SIGUSR1 signal.\n");  
        // 这里可以执行一些操作,比如清理资源、改变程序行为等  
    }  
}  
  
int main() {  
    // 注册信号处理函数  
    if (signal(SIGUSR1, signal_handler) == SIG_ERR) {  
        perror("signal");  
        exit(EXIT_FAILURE);  
    }  
  
    printf("Process ID is %d\n", getpid());  
    printf("Sending SIGUSR1 signal to myself...\n");  
  
    // 发送 SIGUSR1 信号给当前进程  
    if (kill(getpid(), SIGUSR1) == -1) {  
        perror("kill");  
        exit(EXIT_FAILURE);  
    }  
  
    // 等待一段时间,让信号处理函数有机会执行  
    sleep(1);  
  
    printf("Program finished.\n");  
    return 0;  
}

在这个示例中,我们首先定义了一个信号处理函数 signal_handler,它将在接收到 SIGUSR1 信号时被调用。我们使用 signal 函数将 SIGUSR1 信号的处理函数设置为 signal_handler

然后,我们在 main 函数中打印出当前进程的 ID,并发送一个 SIGUSR1 信号给当前进程(使用 kill 函数和 getpid 函数获取进程 ID)。发送信号后,我们调用 sleep 函数暂停执行一段时间,以确保信号处理函数 signal_handler 有机会被调用并执行。

最后,我们打印一条消息表示程序结束。

编译并运行这个程序,你应该会看到输出显示进程 ID,然后发送信号,最后信号处理函数被执行并打印出接收到信号的消息。

signal函数是Linux系统编程中用于处理进程接收到的信号的一个关键函数。信号是操作系统发送给进程的一种通知机制,用于告知进程发生了某种事件,比如用户按下了Ctrl+C,或者进程收到了一个终止信号等。signal函数允许我们为特定的信号指定一个处理函数,或者选择忽略该信号,或者恢复为默认的处理方式。

函数的原型如下:

#include <signal.h>  
typedef void (*sighandler_t)(int);  
sighandler_t signal(int signum, sighandler_t handler);

参数说明:

  • signum:这是我们要处理的信号的编号。在Linux系统中,有许多预定义的信号,例如SIGINT(通常是由用户按下Ctrl+C产生的),SIGTERM(通常用于请求进程正常终止),SIGKILL(强制终止进程,不能被捕获或忽略)等。
  • handler:这是我们要设置的信号处理函数,或者是特殊值SIG_IGN(忽略该信号)或SIG_DFL(恢复为默认的信号处理方式)。

返回值:

  • 如果成功,signal函数返回之前为signum设置的信号处理函数的指针。如果出现错误,则返回SIG_ERR

使用signal函数时,需要注意以下几点:

  1. 信号的可移植性:不同的系统对信号的处理可能有所不同,因此在编写需要跨平台运行的代码时,应谨慎使用signal函数。
  2. 信号的安全性:在多线程环境中,signal函数的行为可能不是线程安全的。因此,在多线程程序中,更推荐使用sigaction函数来处理信号,因为它提供了更强大的功能,并且行为更加明确。
  3. 信号处理函数的限制:信号处理函数应该尽可能简单,并且只执行一些必要的操作,因为它们在接收到信号时被异步调用,可能会打断正在执行的代码。

请注意,信号处理函数应该尽可能简单和快速,因为它是在信号发生时中断当前程序执行而调用的。在信号处理函数中执行复杂的操作或调用可能引起阻塞的函数可能是不安全的。此外,在某些系统上,对 signal 函数的调用可能会受到限制,因此更健壮的做法是使用 sigaction 函数来设置信号处理函数。

sigaction 是 Linux 系统调用中用于检查或修改与指定信号相关联的处理动作的函数。它提供了比早期的 signal 函数更强大且更灵活的功能。

sigaction 的原型如下:

#include <signal.h>  
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

其中:

  • sig 是要查询或修改的信号的编号。
  • act 是一个指向 struct sigaction 的指针,该结构体包含了新的信号处理动作。如果此参数为 NULL,则不改变当前的动作。
  • oldact 也是一个指向 struct sigaction 的指针,用于保存旧的信号处理动作。如果此参数为 NULL,则不保存旧的动作。

struct sigaction 结构体通常包含以下成员:

struct sigaction {  
    void (*sa_handler)(int);               // 信号处理函数的地址  
    void (*sa_sigaction)(int, siginfo_t *, void *); // 替代的信号处理函数  
    sigset_t sa_mask;                      // 在信号处理函数执行期间要阻塞的信号集  
    int sa_flags;                          // 控制信号处理行为的标志位  
    void (*sa_restorer)(void);            // 很少使用,通常设为 NULL  
};

使用 sigaction 时,需要特别注意 sa_flags 成员,它控制信号处理的各种选项。常见的 sa_flags 标志包括:

  • SA_SIGINFO:如果设置了此标志,那么 sa_sigaction 成员将被用作信号处理函数,而不是 sa_handlersa_sigaction 允许信号处理函数接收额外的信息,如发送信号的进程 ID 和信号值。
  • SA_RESETHAND:当捕获到信号时,此标志会在调用处理函数之前将信号的处理方式重置为默认动作(即 SIG_DFL)。
  • SA_NODEFERSA_NOMASK:通常,当信号处理函数正在执行时,它正在处理的信号会被自动阻塞,直到处理函数返回。设置此标志会改变这种行为,允许处理函数在执行期间再次接收相同的信号。
  • SA_ONSTACK:此标志指示如果为信号设置了备选信号栈(通过 sigaltstack),则在此信号的处理过程中应使用备选栈。

在设置 sigaction 时,你可能希望使用 SA_SIGINFO 标志,以便能够接收关于信号的额外信息。这通常是通过定义一个带有三个参数的函数,并将该函数的地址赋给 sa_sigaction 来实现的。这三个参数是信号编号、一个 siginfo_t 结构体(包含关于信号的详细信息)和一个指向 ucontext_t 的指针(包含关于接收信号的上下文的信息)。

以下是一个简单的示例,展示了如何使用 sigaction 来设置一个信号处理函数,该函数能够接收信号的额外信息:

#include <stdio.h>  
#include <stdlib.h>  
#include <signal.h>  
#include <unistd.h>  
  
void signal_handler(int sig, siginfo_t *info, void *context) {  
    printf("Received signal %d from process %ld\n", sig, (long)info->si_pid);  
}  
  
int main() {  
    struct sigaction sa;  
    sa.sa_sigaction = signal_handler;  
    sa.sa_flags = SA_SIGINFO;  
    sigemptyset(&sa.sa_mask);  
    if (sigaction(SIGUSR1, &sa, NULL) == -1) {  
        perror("sigaction");  
        exit(EXIT_FAILURE);  
    }  
  
    // 等待信号,或者执行其他任务  
    while (1) {  
        pause();  
    }  
  
    return 0;  
}

在这个示例中,我们定义了一个 signal_handler 函数,它接受三个参数,并打印出接收到的信号编号以及发送信号的进程的 ID。然后,我们设置 sigaction 结构体,将 sa_sigaction 指向我们的处理函数,并设置 sa_flagsSA_SIGINFO。最后,我们调用 sigaction 来安装新的信号处理动作。程序随后进入一个无限循环,等待接收信号。当接收到 SIGUSR1 信号时,我们的处理函数会被调用。

消息队列

当使用Linux C进行消息队列编程时,可以使用POSIX消息队列接口。下面是一个简单的示例代码,展示了如何在两个进程之间使用消息队列进行通信。

首先,我们创建一个发送消息的进程(sender.c):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
#include <mqueue.h>  
  
#define MSG_SIZE 256  
#define QUEUE_NAME "/my_mq"  
  
int main() {  
    mqd_t mqdes;  
    char msg_buffer[MSG_SIZE];  
    struct mq_attr attr;  
      
    // 设置消息队列属性  
    attr.mq_flags = 0;  
    attr.mq_maxmsg = 10; // 队列中最大消息数  
    attr.mq_msgsize = MSG_SIZE; // 最大消息大小  
    attr.mq_curmsgs = 0; // 当前队列中的消息数  
  
    // 打开消息队列,如果不存在则创建  
    mqdes = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0644, &attr);  
    if (mqdes == (mqd_t) -1) {  
        perror("mq_open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 发送消息  
    strcpy(msg_buffer, "Hello from sender!");  
    if (mq_send(mqdes, msg_buffer, strlen(msg_buffer) + 1, 0) == -1) {  
        perror("mq_send");  
        exit(EXIT_FAILURE);  
    }  
    printf("Sent message: %s\n", msg_buffer);  
  
    // 关闭消息队列  
    mq_close(mqdes);  
    return 0;  
}

然后,我们创建一个接收消息的进程(receiver.c):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
#include <mqueue.h>  
  
#define MSG_SIZE 256  
#define QUEUE_NAME "/my_mq"  
  
int main() {  
    mqd_t mqdes;  
    char msg_buffer[MSG_SIZE];  
    ssize_t bytes_read;  
    unsigned int prio;  
  
    // 打开消息队列  
    mqdes = mq_open(QUEUE_NAME, O_RDONLY);  
    if (mqdes == (mqd_t) -1) {  
        perror("mq_open");  
        exit(EXIT_FAILURE);  
    }  
  
    // 接收消息  
    bytes_read = mq_receive(mqdes, msg_buffer, MSG_SIZE, &prio);  
    if (bytes_read == -1) {  
        perror("mq_receive");  
        exit(EXIT_FAILURE);  
    }  
    printf("Received message: %s\n", msg_buffer);  
  
    // 关闭消息队列  
    mq_close(mqdes);  
    return 0;  
}

编译并运行这两个程序之前,请确保您的系统支持POSIX消息队列。编译时,您可能需要链接-lrt库,因为它包含了消息队列相关的函数。

编译发送者和接收者程序:

gcc sender.c -o sender -lrt  
gcc receiver.c -o receiver -lrt

首先运行接收者程序,因为它需要首先打开消息队列以准备接收消息。然后,运行发送者程序来发送消息。

./receiver & # 在后台运行接收者  
./sender     # 运行发送者发送消息

如果一切正常,您应该会在接收者的输出中看到发送者发送的消息。

请注意,在真实环境中,您可能还需要处理更多错误情况,比如检查mq_sendmq_receive的返回值,以及处理在发送和接收消息时可能出现的阻塞情况。此外,确保在生产代码中适当地清理和删除不再需要的消息队列。

共享内存

在Linux中,共享内存允许两个或多个进程共享同一块内存区域。这种机制通常用于高效的进程间通信(IPC)。下面是一个简单的示例,展示了如何在两个进程之间使用共享内存。

首先,我们创建一个创建和设置共享内存的进程(server.c):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <sys/types.h>  
  
#define SHM_SIZE 1024 // 共享内存大小  
  
int main() {  
    key_t key;  
    int shmid;  
    char *shm, *s;  
  
    // 生成一个唯一的key  
    if ((key = ftok(".", 'R')) == -1) {  
        perror("ftok");  
        exit(1);  
    }  
  
    // 创建共享内存  
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {  
        perror("shmget");  
        exit(1);  
    }  
  
    // 将共享内存附加到当前进程的地址空间  
    if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {  
        perror("shmat");  
        exit(1);  
    }  
  
    // 向共享内存写入数据  
    s = shm;  
    strcpy(s, "Hello from shared memory!");  
  
    // 等待客户端读取数据  
    printf("Server: Shared memory created and data written.\n");  
    printf("Server: Waiting for client to attach and read the data...\n");  
    getchar(); // 等待用户按键  
  
    // 分离共享内存  
    if (shmdt(shm) == -1) {  
        perror("shmdt");  
        exit(1);  
    }  
  
    // 删除共享内存  
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {  
        perror("shmctl");  
        exit(1);  
    }  
  
    return 0;  
}

然后,我们创建一个读取共享内存的进程(client.c):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <sys/types.h>  
  
#define SHM_SIZE 1024 // 共享内存大小  
  
int main() {  
    key_t key;  
    int shmid;  
    char *shm, *s;  
  
    // 生成与服务器相同的key  
    if ((key = ftok(".", 'R')) == -1) {  
        perror("ftok");  
        exit(1);  
    }  
  
    // 尝试获取共享内存  
    if ((shmid = shmget(key, SHM_SIZE, 0666)) == -1) {  
        perror("shmget");  
        exit(1);  
    }  
  
    // 将共享内存附加到当前进程的地址空间  
    if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {  
        perror("shmat");  
        exit(1);  
    }  
  
    // 从共享内存读取数据  
    s = shm;  
    printf("Client: Read from shared memory: %s\n", s);  
  
    // 分离共享内存  
    if (shmdt(shm) == -1) {  
        perror("shmdt");  
        exit(1);  
    }  
  
    return 0;  
}

编译和运行这两个程序:

gcc -o server server.c  
gcc -o client client.c  
  
# 在一个终端中运行server  
./server  
  
# 在另一个终端中运行client  
./client

运行server程序后,它会在共享内存中写入一条消息并等待。运行client程序后,它会连接到共享内存并读取消息。

请注意,这个示例是非常基本的,并且在实际应用中,共享内存的使用需要更复杂的错误处理和同步机制(如信号量或互斥锁),以确保数据的一致性和安全性。此外,还需要确保在程序退出时正确地清理共享内存资源。

信号量

在Linux中,信号量(semaphores)是一种用于同步进程之间对共享资源的访问的机制。信号量维护一个整数值,该值表示可用资源的数量。当一个进程需要访问共享资源时,它会尝试减少信号量的值;如果信号量的值大于0,则访问被允许,并且信号量的值减1。如果信号量的值为0,则进程将被阻塞,直到信号量的值变为正数。

下面是一个简单的 Linux C 示例代码,展示了如何使用信号量(semaphores)来同步两个进程对共享资源的访问。这个例子中,我们创建了一个共享内存区域,并使用信号量来确保在任何时候只有一个进程可以写入共享内存。

首先,创建一个头文件 semaphore_example.h 来定义共享内存和信号量的键:

// semaphore_example.h  
#ifndef SEMAPHORE_EXAMPLE_H  
#define SEMAPHORE_EXAMPLE_H  
  
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <sys/sem.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
  
#define SHM_SIZE 1024 // 共享内存大小  
#define SEM_KEY 1234  // 信号量的key  
  
#endif // SEMAPHORE_EXAMPLE_H

接下来,创建 writer.c 文件,它将作为写入共享内存的进程:

// writer.c  
#include "semaphore_example.h"  
  
int main() {  
    int shmid, semid;  
    key_t shm_key, sem_key;  
    char *shm_ptr;  
    struct sembuf sem_op;  
  
    // 生成共享内存和信号量的key  
    if ((shm_key = ftok(".", 'S')) == -1) {  
        perror("ftok");  
        exit(1);  
    }  
    sem_key = SEM_KEY;  
  
    // 创建共享内存  
    if ((shmid = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {  
        perror("shmget");  
        exit(1);  
    }  
  
    // 将共享内存附加到当前进程的地址空间  
    if ((shm_ptr = shmat(shmid, NULL, 0)) == (char *) -1) {  
        perror("shmat");  
        exit(1);  
    }  
  
    // 创建信号量  
    if ((semid = semget(sem_key, 1, IPC_CREAT | 0666)) == -1) {  
        perror("semget");  
        exit(1);  
    }  
  
    // 初始化信号量值为0,表示没有资源可用  
    if (semctl(semid, 0, SETVAL, 0) == -1) {  
        perror("semctl");  
        exit(1);  
    }  
  
    // 等待信号量变为可用(P操作)  
    sem_op.sem_num = 0;  
    sem_op.sem_op = -1; // 请求一个资源  
    sem_op.sem_flg = 0;  
    if (semop(semid, &sem_op, 1) == -1) {  
        perror("semop");  
        exit(1);  
    }  
  
    // 写入共享内存  
    strcpy(shm_ptr, "Hello from writer!");  
    printf("Writer: Data written to shared memory.\n");  
  
    // 释放信号量(V操作),表示资源现在可用  
    sem_op.sem_op = 1; // 释放一个资源  
    if (semop(semid, &sem_op, 1) == -1) {  
        perror("semop");  
        exit(1);  
    }  
  
    // 分离共享内存  
    if (shmdt(shm_ptr) == -1) {  
        perror("shmdt");  
        exit(1);  
    }  
  
    // 删除共享内存和信号量  
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {  
        perror("shmctl");  
        exit(1);  
    }  
    if (semctl(semid, 0, IPC_RMID) == -1) {  
        perror("semctl");  
        exit(1);  
    }  
  
    return 0;  
}

然后,创建 reader.c 文件,它将作为读取共享内存的进程:

// reader.c  
#include "semaphore_example.h"  
  
int main() {  
    int shmid, semid;  
    key_t shm_key, sem_key;  
    char *shm_ptr;  
    struct sembuf sem_op;  
  
    // 生成共享内存和信号量的key  
    if ((shm_key = ftok(".", 'R')) == -1) {  
        perror("ftok");  
        exit(1);  
    }  
    sem_key = SEM_KEY;  
  
    // 获取共享内存  
    if ((shmid = shmget(shm_key, SHM_SIZE, 0666)) == -1) {  
        perror("shmget");  
        exit(1);  
    }  
  
    // 将共享内存附加到当前进程的地址空间  
    if ((shm_ptr = shmat(shmid, NULL, 0)) == (char *) -1) {  
        perror("shmat");  
        exit(1);  
    }  
  
    // 获取信号量  
    if ((semid = semget(sem_key, 1, 0666)) == -1) {  
        perror("semget");  
        exit(1);  
    }  
  
    // 等待信号量变为可用(P操作),表示资源现在可以访问  
    sem_op.sem_num = 0;  
    sem_op.sem_op = -1; // 请求一个资源  
    sem_op.sem_flg = 0;  
    if (semop(semid, &sem_op, 1) == -1) {  
        perror("semop");  
        exit(1);  
    }  
  
    // 读取共享内存  
    printf("Reader: Data from shared memory: %s\n", shm_ptr);  
  
    // 释放信号量(V操作),表示资源已经被读取,其他进程可以访问  
    sem_op.sem_op = 1; // 释放一个资源  
    if (semop(semid, &sem_op, 1) == -1) {  
        perror("semop");  
        exit(1);  
    }  
  
    // 分离共享内存  
    if (shmdt(shm_ptr) == -1) {  
        perror("shmdt");  
        exit(1);  
    }  
  
    return 0;  
}

请注意,在这个例子中,writer.creader.c 使用不同的 ftok 调用生成的 shm_key 来区分它们各自的共享内存。在实际应用中,你可能需要更复杂的同步机制来确保在 writer 写入共享内存和 reader 读取共享内存之间没有竞态条件。此外,这个例子假设 writerreader 进程几乎同时运行,因此它们可以找到并访问共享内存和信号量。在真实场景中,你可能需要额外的逻辑来处理进程间的通信和同步。

要编译和运行这两个程序,你可以使用如下命令:

gcc -o writer writer.c  
gcc -o reader reader.c  
  
# 首先运行 writer  
./writer  
  
# 然后运行 reader  
./reader

请确保在运行这些程序之前,系统上没有相同键的共享内存和信号量存在,否则 shmgetsemget 可能会返回已经存在的对象。此外,这些程序没有处理可能的竞态条件,因此它们应该在相对受控的环境中运行,以确保正确的行为。在真实的并发环境中,你可能需要使用更复杂的同步机制,比如互斥锁(mutexes)或条件变量(condition variables)。

套接字

下面是一个简单的Linux C示例代码,展示了如何使用UNIX套接字(也称为IPC套接字)进行进程间通信。这个示例包括一个服务器进程和一个客户端进程。服务器进程创建一个套接字,绑定到一个文件路径,然后监听连接。客户端进程连接到服务器,发送一条消息,并接收服务器的响应。

首先是服务器端的代码(server.c):

// server.c  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <sys/un.h>  
  
#define SOCK_PATH "/tmp/mysocket"  
#define BUFFER_SIZE 1024  
  
int main() {  
    int server_fd, client_fd;  
    struct sockaddr_un server_addr, client_addr;  
    socklen_t client_len;  
    char buffer[BUFFER_SIZE];  
    ssize_t bytes_read;  
  
    // 创建UNIX套接字  
    if ((server_fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {  
        perror("socket");  
        exit(EXIT_FAILURE);  
    }  
  
    // 填充服务器地址结构  
    memset(&server_addr, 0, sizeof(server_addr));  
    server_addr.sun_family = AF_UNIX;  
    strncpy(server_addr.sun_path, SOCK_PATH, sizeof(server_addr.sun_path) - 1);  
  
    // 绑定套接字到地址  
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {  
        perror("bind");  
        exit(EXIT_FAILURE);  
    }  
  
    // 监听连接  
    if (listen(server_fd, 5) == -1) {  
        perror("listen");  
        exit(EXIT_FAILURE);  
    }  
  
    // 接受客户端连接  
    client_len = sizeof(client_addr);  
    if ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len)) == -1) {  
        perror("accept");  
        exit(EXIT_FAILURE);  
    }  
  
    // 读取客户端发送的消息  
    bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);  
    if (bytes_read == -1) {  
        perror("read");  
        exit(EXIT_FAILURE);  
    }  
    buffer[bytes_read] = '\0'; // 确保字符串正确结束  
    printf("Received: %s\n", buffer);  
  
    // 向客户端发送响应消息  
    const char* response = "Hello from server!";  
    if (write(client_fd, response, strlen(response)) == -1) {  
        perror("write");  
        exit(EXIT_FAILURE);  
    }  
  
    // 关闭套接字  
    close(client_fd);  
    close(server_fd);  
    unlink(SOCK_PATH); // 删除套接字文件  
  
    return 0;  
}

接下来是客户端的代码(client.c),用于连接到服务器,发送消息并接收响应:

// client.c  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <sys/un.h>  
  
#define SOCK_PATH "/tmp/mysocket"  
#define BUFFER_SIZE 1024  
  
int main() {  
    int client_fd;  
    struct sockaddr_un server_addr;  
    char buffer[BUFFER_SIZE];  
    ssize_t bytes_written, bytes_read;  
  
    // 创建UNIX套接字  
    if ((client_fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {  
        perror("socket");  
        exit(EXIT_FAILURE);  
    }  
  
    // 填充服务器地址结构  
    memset(&server_addr, 0, sizeof(server_addr));  
    server_addr.sun_family = AF_UNIX;  
    strncpy(server_addr.sun_path, SOCK_PATH, sizeof(server_addr.sun_path) - 1);  
  
    // 连接到服务器  
    if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {  
        perror("connect");  
        exit(EXIT_FAILURE);  
    }  
  
    // 向服务器发送消息  
    const char* message = "Hello from client!";  
    bytes_written = write(client_fd, message, strlen(message));  
    if (bytes_written == -1) {  
        perror("write");  
        exit(EXIT_FAILURE);  
    }  
  
    // 从服务器接收响应  
    bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);  
    if (bytes_read == -1) {  
        perror("read");  
        exit(EXIT_FAILURE);  
    }  
    buffer[bytes_read] = '\0'; // 确保字符串正确结束  
    printf("Received: %s\n", buffer);  
  
    // 关闭套接字  
    close(client_fd);  
  
    return 0;  
}

在这个完整的客户端代码中,我们首先创建了一个UNIX套接字,并连接到服务器套接字。然后,我们发送一个消息到服务器,并等待接收服务器的响应。一旦收到响应,我们就将其打印到控制台,并关闭套接字。

请注意,在实际使用中,你可能需要添加额外的错误处理逻辑,以及检查返回值以确保操作成功。此外,你可能还需要确保服务器在客户端尝试连接之前已经启动并监听连接。

编译和运行这些代码时,你需要先编译服务器代码(例如 gcc -o server server.c),然后运行它。一旦服务器开始监听连接,你可以编译并运行客户端代码(例如 gcc -o client client.c),它将连接到服务器并发送消息。

由于UNIX套接字通常通过文件路径来标识,因此你需要确保服务器和客户端使用的路径相同,并且路径对应的文件在服务器开始监听之前不存在(因为bind()调用会创建这个文件)。如果文件已经存在,你可能需要先删除它(可以使用unlink()函数或手动删除)。在服务器关闭连接并退出后,你可能还想删除这个文件,以避免留下不必要的文件残留。

posted @ 2024-03-19 15:52  付时凡  阅读(1085)  评论(0编辑  收藏  举报