【Linux】进程间通信IPC机制

发布于:2024-04-30 ⋅ 阅读:(28) ⋅ 点赞:(0)

一、无名管道

1.只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程)。

2.是一个单工的通信模式,具有固定的读端和写端。

3.管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read()、write()等函数。但是它不属于任何文件系统,并且只存在于内存中。

#include <unistd.h>
int pipe(int filedes[2]);
在管道中,文件描述符数组的第一个元素(索引为0)用于读取,第二个元素(索引为1)用于写入。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1

int main() 
{
    char write_msg[BUFFER_SIZE] = "Hello, child!";
    char read_msg[BUFFER_SIZE];

    int fd[2];
    pid_t pid;

    // 创建管道
    if (pipe(fd) == -1) 
    {
        fprintf(stderr, "Pipe failed");
        return 1;
    }

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

    if (pid < 0) 
    {
        fprintf(stderr, "Fork failed");
        return 1;
    }
    // 父进程
    if (pid > 0)   
    {
        // 关闭写入端,因为父进程不会写入
        close(fd[WRITE_END]);

        // 从管道中读取消息
        read(fd[READ_END], read_msg, BUFFER_SIZE);
        printf("Parent received message from child: %s\n", read_msg);

        // 关闭读取端
        close(fd[READ_END]);
    } 
    // 子进程
    else 
    {  
        // 关闭读取端,因为子进程不会读取
        close(fd[READ_END]);

        // 写入消息到管道
        write(fd[WRITE_END], write_msg, BUFFER_SIZE);

        // 关闭写入端
        close(fd[WRITE_END]);
    }

    return 0;
}

./a.out
执行结果:Parent received message from child: Hello, child! 

二、有名管道

  1. mkfifo(const char *pathname, mode_t mode):

    • 这个函数用于创建一个有名管道。
    • 参数pathname是要创建的管道的路径名,mode是指定管道权限的位掩码。
    • 返回值:成功时返回0,失败时返回-1,并设置errno变量表示错误类型。
  2. open(const char *pathname, int flags, mode_t mode):

    • 这个函数用于打开文件或管道。
    • 参数pathname是要打开的文件或管道的路径名,flags指定打开文件的方式(如只读、只写、读写等),mode是当创建新文件时指定的权限。
    • 返回值:成功时返回文件描述符,失败时返回-1,并设置errno变量表示错误类型。
  3. write(int fd, const void *buf, size_t count):

    • 这个函数用于向文件描述符指定的文件或管道中写入数据。
    • 参数fd是打开文件或管道时返回的文件描述符,buf是要写入的数据缓冲区,count是要写入的字节数。
    • 返回值:成功时返回实际写入的字节数,失败时返回-1,并设置errno变量表示错误类型。
  4. read(int fd, void *buf, size_t count):

    • 这个函数用于从文件描述符指定的文件或管道中读取数据。
    • 参数fd是打开文件或管道时返回的文件描述符,buf是用于接收数据的缓冲区,count是要读取的最大字节数。
    • 返回值:成功时返回实际读取的字节数,失败时返回-1,并设置errno变量表示错误类型。
  5. close(int fd):

    • 这个函数用于关闭打开的文件描述符。
    • 参数fd是要关闭的文件描述符。
    • 返回值:成功时返回0,失败时返回-1,并设置errno变量表示错误类型。
  6. unlink(const char *pathname):

    • 这个函数用于删除文件或链接。
    • 参数pathname是要删除的文件或链接的路径名。
    • 返回值:成功时返回0,失败时返回-1,并设置errno变量表示错误类型。

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

#define BUFFER_SIZE 25

int main() 
{
    char *fifo_name = "./name";
    char write_msg[BUFFER_SIZE] = "Hello, world!";
    char read_msg[BUFFER_SIZE];

    // 创建有名管道
    mkfifo(fifo_name, 0666);

    pid_t pid = fork();

    if (pid == 0)// 子进程负责写入
	{
        // 打开管道进行写入
        int fd_write = open(fifo_name, O_WRONLY);
        write(fd_write, write_msg, BUFFER_SIZE);
        close(fd_write);
    } 
	else if (pid > 0) // 父进程负责读取
	{ 
        // 打开管道进行读取
        int fd_read = open(fifo_name, O_RDONLY);
        read(fd_read, read_msg, BUFFER_SIZE);
        printf("Received message: %s\n", read_msg);
        close(fd_read);
    } 
	else  // fork失败
	{
        fprintf(stderr, "Fork failed");
        return 1;
    }

    // 删除管道
    unlink(fifo_name);

    return 0;
}

三、共享内存

共享内存是最高效的,因为避免了数据在用户空间和内核空间的来回拷贝

  1. ftok(const char *pathname, int proj_id):

    • ftok()函数用于生成一个唯一的key,用于创建或访问共享内存。
    • 参数pathname是一个路径名,proj_id是一个整数,用于生成key。
    • 返回值:如果成功,返回一个唯一的key,如果失败,返回-1。
  2. shmget(key_t key, size_t size, int shmflg):

    • shmget()函数用于创建共享内存段或获取共享内存段的标识符。
    • 参数key是由ftok()生成的唯一key,size是共享内存的大小,shmflg是标志位,用于指定权限和行为。
    • 返回值:如果成功,返回共享内存段的标识符,如果失败,返回-1。
  3. shmat(int shmid, const void *shmaddr, int shmflg):

    • shmat()函数用于将共享内存连接到当前进程的地址空间。
    • 参数shmid是共享内存段的标识符,shmaddr通常为NULL(表示系统自动选择地址),shmflg是标志位,通常为0。
    • 返回值:如果成功,返回指向共享内存段的指针,如果失败,返回(void *)-1。
  4. shmdt(const void *shmaddr):

    • shmdt()函数用于将共享内存从当前进程的地址空间分离。
    • 参数shmaddr是指向共享内存段的指针。
    • 返回值:如果成功,返回0,如果失败,返回-1。
  5. shmctl(int shmid, int cmd, struct shmid_ds *buf):

    • shmctl()函数用于对共享内存进行控制操作,如删除共享内存段。
    • 参数shmid是共享内存段的标识符,cmd是控制命令,buf是一个指向shmid_ds结构体的指针,用于获取共享内存的状态信息。
    • 返回值:如果成功,返回0,如果失败,返回-1。

读端

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

#define SHM_SIZE 1024

int main() 
{
    key_t key = ftok("/", 'R'); // 生成共享内存的key

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

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

    // 写入数据到共享内存
    strcpy(shmaddr, "Hello, shared memory!");

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

    printf("数据已写入共享内存\n");

    return 0;
}

 写端

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

#define SHM_SIZE 1024

int main() 
{
    key_t key = ftok("/", 'R'); // 生成共享内存的key

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

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

    printf("从共享内存中读取到的消息:%s\n", shmaddr);

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

    return 0;
}

写端执行后:

数据已写入共享内存

读端执行后:

从共享内存中读取到的消息:Hello, shared memory! 

四、信号量

1.信号量实现互斥

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define KEY 1234 // 信号量的键值

// 定义一个联合体,用于semctl初始化
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// 创建一个二值信号量并初始化为1
int create_semaphore() 
{
    // 创建一个信号量集,包含1个信号量
    int semid = semget(KEY, 1, IPC_CREAT | 0666); 
    if (semid == -1) {
        perror("semget");
        exit(1);
}

    union semun arg;
    arg.val = 1; // 初始值为1,表示资源可用
    if (semctl(semid, 0, SETVAL, arg) == -1) // 初始化信号量
	{
        perror("semctl");
        exit(1);
    }

    return semid;
}

// P操作(等待资源)
void P(int semid) 
{
    struct sembuf op;
    op.sem_num = 0; // 信号量集中的第一个信号量
    op.sem_op = -1; // 对信号量执行P操作
    op.sem_flg = 0;
    if (semop(semid, &op, 1) == -1) 
	{
        perror("semop");
        exit(1);
    }
}

// V操作(释放资源)
void V(int semid) 
{
    struct sembuf op;
    op.sem_num = 0; // 信号量集中的第一个信号量
    op.sem_op = 1; // 对信号量执行V操作
    op.sem_flg = 0;
    if (semop(semid, &op, 1) == -1) 
	{
        perror("semop");
        exit(1);
    }
}

int main()
{
    int semid = create_semaphore(); // 创建信号量

    pid_t pid = fork(); // 创建子进程
    if (pid == -1) 
	{
        perror("fork");
        exit(1);
    }

    if (pid == 0)  // 子进程
	{
        P(semid); // 等待资源
        printf("Child process: Counter decremented by 1.\n");
        V(semid); // 释放资源
    } 
	else // 父进程
	{
        sleep(1); // 让子进程有机会先运行
        P(semid); // 等待资源
        printf("Parent process: Counter incremented by 1.\n");
        V(semid); // 释放资源
    }

    return 0;
}
  1. semget(key_t key, int nsems, int semflg)

    • 这个函数用于创建一个新的信号量集或获取一个现有信号量集的标识符。
    • key是一个用于唯一标识信号量集的键值。
    • nsems指定了信号量集中的信号量数量。
    • semflg是一组标志,用于指定创建信号量集的权限和行为。
    • 返回值:成功时返回信号量集的标识符,失败时返回-1。
  2. semctl(int semid, int semnum, int cmd, union semun arg)

    • 这个函数用于对信号量集进行控制操作,如初始化、设置值、获取值等。
    • semid是信号量集的标识符。
    • semnum是指定的信号量在集合中的索引,通常为0。
    • cmd是指定要执行的控制命令。
    • arg是一个联合体,用于传递控制命令的参数。
    • 返回值:根据控制命令不同而不同。
  3. semop(int semid, struct sembuf *sops, size_t nsops)

    • 这个函数用于执行一组信号量操作,如等待资源(P操作)和释放资源(V操作)。
    • semid是信号量集的标识符。
    • sops是一个指向信号量操作结构体数组的指针。
    • nsops是指定的信号量操作结构体数组的大小。
    • 返回值:成功时返回0,失败时返回-1。
  4. struct sembuf

    • 这是一个结构体,用于描述信号量操作。
    • 它包含了三个字段:
      • sem_num:信号量集中的信号量索引。
      • sem_op:信号量操作,通常是-1(P操作,等待资源)或1(V操作,释放资源)。
      • sem_flg:信号量操作的标志位,通常为0。
  5. union semun

    • 这是一个联合体,用于传递给semctl()函数的参数。
    • 它包含了多个字段,其中的一个字段可以根据不同的控制命令来使用,比如用于设置或获取信号量值时使用的val字段,用于获取信号量状态信息时使用的buf字段等

  1. create_semaphore():

    • 这个函数用于创建一个二值信号量,并将其初始化为1。
    • 首先,它调用 semget() 函数创建一个包含一个信号量的信号量集,如果创建失败则会打印错误信息并退出程序。
    • 然后,它使用 semctl() 函数将信号量的值初始化为1,表示资源可用。
    • 最后,它返回创建的信号量集的标识符。
  2. P(int semid):

    • 这个函数用于执行 P 操作,即等待资源。
    • 首先,它定义了一个 struct sembuf 结构体 op,用于描述信号量操作。
    • 然后,它将 op.sem_num 设置为0,表示信号量集中的第一个信号量。
    • 接着,它将 op.sem_op 设置为-1,表示对信号量执行 P 操作。
    • 最后,它调用 semop() 函数执行信号量操作,如果操作失败则会打印错误信息并退出程序。
  3. V(int semid):

    • 这个函数用于执行 V 操作,即释放资源。
    • 它的实现与 P() 函数类似,只是将 op.sem_op 设置为1,表示对信号量执行 V 操作。

2.信号量实现同步互斥 

交替实现奇偶数打印,打印一个奇数后必须是一个偶数

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define KEY 1234 // 信号量的键值

// 定义一个联合体,用于semctl初始化
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// 创建一个信号量并初始化为1
int create_semaphore() 
{
    int semid = semget(KEY, 1, IPC_CREAT | 0666); // 创建一个信号量集,包含1个信号量
    if (semid == -1) 
	{
        perror("semget");
        exit(1);
    }

    union semun arg;
    arg.val = 0; // 初始值为0
    if (semctl(semid, 0, SETVAL, arg) == -1) // 初始化信号量
    { 
        perror("semctl");
        exit(1);
    }

    return semid;
}

// P操作(等待资源)
void P(int semid) 
{
    struct sembuf op;
    op.sem_num = 0; // 信号量集中的第一个信号量
    op.sem_op = -1; // 对信号量执行P操作
    op.sem_flg = 0;
    if (semop(semid, &op, 1) == -1) 
	{
        perror("semop");
        exit(1);
    }
}

// V操作(释放资源)
void V(int semid) 
{
    struct sembuf op;
    op.sem_num = 0; // 信号量集中的第一个信号量
    op.sem_op = 1; // 对信号量执行V操作
    op.sem_flg = 0;
    if (semop(semid, &op, 1) == -1) 
	{
        perror("semop");
        exit(1);
    }
}

int main() 
{
	int  i = 1;
	int j = 2;
    int semid = create_semaphore(); // 创建信号量

    pid_t pid = fork(); // 创建子进程
    if (pid == -1) 
	{
        perror("fork");
        exit(1);
    }

    if (pid == 0)  // 子进程打印奇数
	{
		while(1)
		{ 		
           	printf("i = %d\n",i);
			i += 2;
            V(semid); // 释放资源
			sleep(1);
		}       
    } 
	else // 父进程打印偶数
	{
     	while(1)
		{
            P(semid); // 等待资源
			printf("j = %d\n",j);
			j += 2;
		}		
         
    }

    return 0;
}

执行结果

i = 1
j = 2
i = 3
j = 4
i = 5
j = 6
i = 7
j = 8
i = 9
j = 10
i = 11
j = 12
i = 13
j = 14
i = 15
j = 16
 

五、消息队列

发送进程

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

#define MSG_KEY 1234 // 消息队列的键值

// 定义消息结构体
struct msg_buffer 
{
    long msg_type; // 消息类型
    char msg_text[100]; // 消息内容
};

int main() 
{
    int msgid;
    struct msg_buffer message_send;

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

    // 准备发送的消息
    strcpy(message_send.msg_text, "Hello, Message Queue!");
    message_send.msg_type = 1; // 消息类型为1

    // 发送消息
    if (msgsnd(msgid, &message_send, sizeof(message_send) - sizeof(long), 0) == -1) 
	{
        perror("msgsnd");
        exit(1);
    }
    printf("Parent process sent message: %s\n", message_send.msg_text);

    return 0;
}

 接收进程

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

#define MSG_KEY 1234 // 消息队列的键值

// 定义消息结构体
struct msg_buffer {
    long msg_type; // 消息类型
    char msg_text[100]; // 消息内容
};

int main() 
{
    int msgid;
    struct msg_buffer message_rcv;

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

    // 接收消息
    if (msgrcv(msgid, &message_rcv, sizeof(message_rcv) - sizeof(long), 1, 0) == -1) 
	{
        perror("msgrcv");
        exit(1);
    }
    printf("Child process received message: %s\n", message_rcv.msg_text);

    return 0;
}

执行结果:

./s

Parent process sent message: Hello, Message Queue!
./r
Child process received message: Hello, Message Queue!
 

在这两个示例代码中,主要使用了以下系统调用:

  1. msgget: 用于创建或打开一个消息队列。它接受一个参数,即消息队列的键值(一个唯一的标识符),并返回一个消息队列的标识符(msgid)。

  2. msgsnd: 用于向消息队列发送消息。它接受四个参数:消息队列的标识符(msgid)、指向要发送的消息的指针、消息的大小(不包括消息类型字段的大小)、消息的标志(通常为0)。

  3. msgrcv: 用于从消息队列接收消息。它接受五个参数:消息队列的标识符(msgid)、指向用于接收消息的缓冲区的指针、缓冲区大小(不包括消息类型字段的大小)、消息类型(通常为正整数)、接收消息的标志(通常为0)。

  4. msgctl: 用于控制消息队列的属性,如删除消息队列。在这个示例中,我们使用它来删除消息队列。它接受三个参数:消息队列的标识符(msgid)、命令(IPC_RMID 表示删除消息队列)、指向用于控制消息队列属性的结构体的指针(在这个例子中不需要)。

六、套接字

网络部分的内容