进程/线程间通信方式

一、 进程间通信

常见进程间通信方式包括管道,命名管道,共享内存,信号量,消息队列,套接字和信号七种。

1. 管道

管道(Pipe):单向通信,只能在具有亲缘关系的进程之间使用。
头文件

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

使用管道需要经过以下步骤:

  1. 定义一个int类型的数组来存储管道的读写端,例如:int fd[2]。
  2. 调用pipe函数创建管道:int pipe(int fd[2])。
  3. 在父进程中,关闭管道的读端或写端,然后向管道写入数据:write(fd[1], buffer, size)。
  4. 在子进程中,关闭另一端的管道,然后从管道读取数据:read(fd[0], buffer, size)。
  5. 不使用管道时关闭读写端:close(fd[0])和close(fd[1])
注意事项:
  • 管道是单向的,因此需要分别打开读写端。

  • 如果写入的数据超出了管道的缓冲区大小,将会被截断。

  • 使用管道时,要保证读写顺序正确,否则可能会导致死锁等问题。

    下面是一个简单的 C 语言管道使用示例代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main() {
        int pipefd[2];
        pid_t pid;
    
        if (pipe(pipefd) == -1) {
            perror("pipe");
            exit(EXIT_FAILURE);
        }
    
        pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        }
    
        if (pid == 0) { /* Child process */
            close(pipefd[1]); /* Close unused write end */
            char buf;
            while (read(pipefd[0], &buf, 1) > 0)
                write(STDOUT_FILENO, &buf, 1);
            write(STDOUT_FILENO, "\n", 1);
            close(pipefd[0]);
            _exit(EXIT_SUCCESS);
    
        } else { /* Parent process */
            close(pipefd[0]); /* Close unused read end */
            const char *msg = "Hello, world!";
            write(pipefd[1], msg, strlen(msg));
            close(pipefd[1]); /* Reader will see EOF */
            wait(NULL); /* Wait for child */
            exit(EXIT_SUCCESS);
        }
    }
    

这个程序创建了一个管道,然后创建了一个子进程,子进程从管道读取数据并在标准输出上打印出来,父进程向管道写入一条消息。当父进程完成写入后,它关闭管道的写端,这会导致子进程结束循环并退出。父进程等待子进程结束后也退出。

2. 命名管道

命名管道(Named Pipe):允许无亲缘关系的进程间进行通信。

使用方法
  1. 使用mkfifo函数创建一个命名管道文件

    mkfifo("/tmp/myfifo", 0666);
    
  2. 打开命名管道文件,可以使用open函数

    int fd = open("/tmp/myfifo", O_WRONLY);
    
  3. 写入数据到命名管道,可以使用write函数

    char *msg = "Hello, world!";
    write(fd, msg, strlen(msg)+1);
    
  4. 关闭命名管道文件,可以使用close函数

    close(fd);
    
  5. 在另一个进程中读取命名管道中的数据,也需要打开命名管道文件,可以使用open函数

    int fd = open("/tmp/myfifo", O_RDONLY);
    
  6. 从命名管道中读取数据,可以使用read函数

    char buf[1024];
    read(fd, buf, sizeof(buf));
    printf("Received message: %s\n", buf);
    
  7. 关闭命名管道文件,可以使用close函数

    close(fd);
    
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define FIFO_NAME "/tmp/myfifo"

int main() {
    int fd;
    char buf[256];

    mkfifo(FIFO_NAME, 0666); // 创建命名管道

    printf("Waiting for a writer...\n");
    fd = open(FIFO_NAME, O_RDONLY); // 打开命名管道并等待写入者

    printf("Received data: ");
    while (read(fd, buf, sizeof(buf)) > 0) { // 从命名管道中读取数据
        printf("%s", buf);
    }
    printf("\n");

    close(fd);
    unlink(FIFO_NAME); // 关闭并删除命名管道

    return 0;
}

3. 共享内存

共享内存(Shared Memory):多个进程可以访问同一块物理内存。

使用方法
  1. 创建共享内存区域:使用shmget函数创建一个共享内存区域,并返回一个唯一的标识符。
  2. 连接共享内存区域:使用shmat函数将进程连接到共享内存区域。该函数返回一个指向共享内存区域的指针,可以通过该指针访问共享内存区域。
  3. 访问共享内存:通过指针访问共享内存区域,可以读取和修改共享内存中的数据。
  4. 分离共享内存区域:使用shmdt函数将进程与共享内存区域分离。
  5. 删除共享内存区域:使用shmctl函数删除共享内存区域。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024

int main() {
    int shmid;
    key_t key;
    char *shm, *s;

    // 创建共享内存段
    key = 1234;
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) < 0) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存连接到进程地址空间
    if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {
        perror("shmat");
        exit(1);
    }

    // 在共享内存中写入数据
    strncpy(shm, "Hello, World!", SHM_SIZE);

    // 读取共享内存中的数据
    for (s = shm; *s != '\0'; s++) {
        putchar(*s);
    }
    putchar('\n');

    // 解除与进程的连接
    shmdt(shm);

    // 删除共享内存段
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

注意事项

在使用共享内存时需要注意同步问题,避免多个进程同时修改共享内存数据导致冲突。可以使用信号量或互斥锁等机制来解决同步问题。

4. 信号量

信号量(Semaphore):用于控制对共享资源的访问,防止出现竞态条件。

使用方法
  1. 包含头文件:#include <semaphore.h>

  2. 定义信号量变量:sem_t sem;

  3. 初始化信号量:sem_init(&sem, 0, init_value);,其中init_value为信号量初值。

  4. 使用信号量进行同步操作:

    • 等待信号量:sem_wait(&sem);,如果信号量的值大于0,则将信号量的值减1;否则阻塞等待。

    • 发送信号量:sem_post(&sem);,将信号量的值加1。

  5. 销毁信号量:sem_destroy(&sem);

示例代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;

void* thread_func(void* arg) {
    sem_wait(&sem);  // 等待信号量变为非零值
    printf("Thread %ld is running!\n", pthread_self());
    sem_post(&sem);  // 增加信号量值
    return NULL;
}

int main() {
    int i;
    pthread_t tid[5];
    
    // 初始化信号量
    sem_init(&sem, 0, 1);
    
    for (i = 0; i < 5; i++) {
        pthread_create(&tid[i], NULL, &thread_func, NULL);
    }
    
    // 延迟主线程执行,以保证子线程已经启动并等待信号量
    sleep(1);

    sem_post(&sem);  // 增加信号量值
    
    for (i = 0; i < 5; i++) {
        pthread_join(tid[i], NULL);
    }
    
    // 销毁信号量
    sem_destroy(&sem);
    
    return 0;
}

注意事项
  • 信号量只能在进程内共享,不能跨进程共享。
  • 信号量的初始值应该正确设置,避免出现死锁或饥饿的情况。
  • 在使用信号量时应当保证原子性,避免多个进程同时操作信号量导致竞态条件的发生。

5. 消息队列

消息队列(Message Queue):消息发送者将消息发送到队列中,接收者从队列中取出消息。

使用方法
  1. 包含头文件:

    #include <sys/msg.h>
    
  2. 定义消息结构体,例如:

    struct msgbuf {
    long mtype;     // 消息类型
    char mtext[1024];  // 消息内容
    };
    
  3. 调用msgget函数创建一个消息队列,例如:

    int msgid = msgget(key, IPC_CREAT|0666);
    
  4. 使用msgsnd函数往消息队列发送消息,例如:

    struct msgbuf message;
    message.mtype = 1;
    strcpy(message.mtext, "Hello World");
    msgsnd(msgid, &message, sizeof(message.mtext), 0);
    
  5. 使用msgrcv函数从消息队列接收消息,例如:

    struct msgbuf message;
    msgrcv(msgid, &message, sizeof(message.mtext), 1, 0);
    printf("%s\n", message.mtext);
    
  6. 使用msgctl函数删除消息队列,例如:

    msgctl(msgid, IPC_RMID, 0);
    
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_SIZE 1024

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main() {
    key_t key;
    int msgid;
    struct msgbuf message;

    // 创建或获取一个消息队列
    key = ftok("msgqtest", 'b');
    msgid = msgget(key, 0666 | IPC_CREAT);

    if (msgid == -1) {
        perror("msgget");
        exit(1);
    }

    printf("Message Queue ID: %d\n", msgid);

    // 发送一条消息到消息队列
    message.mtype = 1;
    sprintf(message.mtext, "Hello, Message Queue!");
    msgsnd(msgid, &message, sizeof(message), 0);

    // 接收并打印队列中的消息
    msgrcv(msgid, &message, sizeof(message), 0, 0);
    printf("Received Message: %s\n", message.mtext);

    // 删除消息队列
    msgctl(msgid, IPC_RMID, NULL);

    return 0;
}
注意事项
  • 消息格式应该简单、清晰,并且易于处理。消息内容应该只包含必要的信息。
  • 在选择消息队列软件时,需要考虑其性能、可靠性、扩展性和管理性等方面。
  • 使用适当的队列长度限制来避免内存溢出问题。
  • 处理消息时要确保线程安全,防止多个线程同时访问同一条消息。
  • 对于高并发场景,可以使用分布式消息队列来提高性能。
  • 注意消息队列中的消息顺序,确保按照正确的顺序进行处理。
  • 当消息队列出现故障时,需要有相应的容错机制。
  • 需要定期监控消息队列的状态,及时发现并解决潜在的问题。

6. 套接字

套接字(Socket):适用于不同计算机或操作系统之间的进程通信。

使用方法
  1. 创建socket:调用socket函数,指定协议族、类型和协议参数,创建一个套接字描述符。

  2. 绑定socket:调用bind函数,将套接字描述符绑定到本地IP地址和端口号上。

  3. 监听socket(可选):调用listen函数,将套接字设置为监听状态,等待客户端发起连接请求。

  4. 接受连接(如果有客户端连接):调用accept函数,等待客户端连接请求,并返回一个新的套接字描述符,用于与该客户端进行通信。

  5. 进行通信:使用recv函数从套接字中接收数据,使用send函数向套接字发送数据。

  6. 关闭套接字:使用close函数关闭套接字。

示例代码

服务器端

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
    // 1. 创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 将socket()返回值和本地的IP端口绑定到一起
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);   // 大端端口
    // INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
    // 这个宏可以代表任意一个IP地址
    // 这个宏一般用于本地的绑定操作
    addr.sin_addr.s_addr = INADDR_ANY;  // 这个宏的值为0 == 0.0.0.0
//    inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }

    // 3. 设置监听
    ret = listen(lfd, 128);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }

    // 4. 阻塞等待并接受客户端连接
    struct sockaddr_in cliaddr;
    int clilen = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
    if(cfd == -1)
    {
        perror("accept");
        exit(0);
    }
    // 打印客户端的地址信息
    char ip[24] = {0};
    printf("客户端的IP地址: %s, 端口: %d\n",
           inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)),
           ntohs(cliaddr.sin_port));

    // 5. 和客户端通信
    while(1)
    {
        // 接收数据
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        int len = read(cfd, buf, sizeof(buf));
        if(len > 0)
        {
            printf("客户端say: %s\n", buf);
            write(cfd, buf, len);
        }
        else if(len  == 0)
        {
            printf("客户端断开了连接...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    close(cfd);
    close(lfd);

    return 0;
}

客户端

// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
    // 1. 创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 连接服务器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);   // 大端端口
    inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);

    int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("connect");
        exit(0);
    }

    // 3. 和服务器端通信
    int number = 0;
    while(1)
    {
        // 发送数据
        char buf[1024];
        sprintf(buf, "你好, 服务器...%d\n", number++);
        write(fd, buf, strlen(buf)+1);
        
        // 接收数据
        memset(buf, 0, sizeof(buf));
        int len = read(fd, buf, sizeof(buf));
        if(len > 0)
        {
            printf("服务器say: %s\n", buf);
        }
        else if(len  == 0)
        {
            printf("服务器断开了连接...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
        sleep(1);   // 每隔1s发送一条数据
    }

    close(fd);

    return 0;
}
注意事项
  • 确保正确设置套接字类型,如TCP或UDP。
  • 在使用套接字前,必须先创建套接字并绑定地址。
  • 使用recv()和send()等函数时,要确保正确处理返回值,以防止数据丢失或错误。
  • 对于长时间运行的套接字连接,应该设置超时以避免挂起。
  • 在使用多线程时,确保在共享资源上进行同步以避免竞争条件。
  • 避免使用过多的套接字,以防止系统资源耗尽。
  • 了解不同操作系统和网络环境之间可能存在的差异,以便编写可移植的代码。
  • 在开发网络应用程序时,始终考虑安全性和鲁棒性。

7.信号

信号(Signal):进程会收到一个信号,可以用来通知进程发生了某个事件。

使用方法
  • 定义一个信号处理函数,用于在收到特定信号时执行操作。
  • 注册信号处理函数,以便在收到信号时调用它。
  • 发送信号,可使用系统函数如 kill() 或 raise()。
  • 等待信号,可以使用系统函数如 pause() 或 sigsuspend()。
示例代码
#include <stdio.h>
#include <signal.h>

void sigint_handler(int signo) {
    printf("Received SIGINT signal.\n");
}

int main() {
    signal(SIGINT, sigint_handler);
    printf("Waiting for a SIGINT signal...\n");
    while(1) {
        // do nothing and wait for signal
    }
    return 0;
}
注意事项

二、 线程间通信

线程间常用的通信方式有互斥锁,读写锁,条件变量,自旋锁和屏障

1. 互斥锁(Mutex)

控制对共享资源的访问,确保同一时刻只有一个线程可以访问该资源。

2. 读写锁(Read-Write Lock)

允许多个线程同时读取共享资源,但在写入时必须互斥。

3. 条件变量(Condition Variable)

让线程等待某些条件满足后再继续执行,用于线程间通信和同步。

4. 自旋锁(Spin Lock)

在使用短时间内的锁保护共享资源时,相比于互斥锁,自旋锁不会导致线程阻塞,而是一直进行忙等待直到获得锁。

5. 屏障(Barrier)

让多个线程在特定点处等待彼此,直到所有线程都到达这个点才能继续执行后面的代码。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值