Linux 进程通信

发布时间:2023年12月20日

补充说明部分为相关函数和不太重要的概念介绍

匿名管道

匿名管道使用

使用方法一:
使用函数介绍:

#include <unistd.h>
功能:创建一无名管道
原型:
int pipe(int fd[2]);
参数:
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

简单的父子进程通过管道通信范例:

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

 int main() 
 {
 	int pipefd[2]; 
 	if (pipe(pipefd) == -1)
 	{
 	 perror("pipe error");
 	 exit(-1);
 	}
 	pid_t pid;
 	pid = fork();
 	
 	if (pid == -1)
 	{
 		perror("fork error");
 		exit(-2);
 	}
 	if (pid == 0) 
 	{
 		close(pipefd[0]);
 		write(pipefd[1], "hello", 5);
 		close(pipefd[1]);
 		exit(1); //子进程成功运行
 	}
 	close(pipefd[1]);
 	char buf[10] = {0};
 	//read(pipefd[0], buf, 10);
	while(read(pipefd[0], buf, 10)) 
	{
		// 这样写的原因,见匿名管道的读写部分。
	}

 	printf("buf=%s\n", buf);
 
 	return 0;
}

使用方法二:
使用管道操作符 |
Linux命令行中的管道操作符本质上就是创建了一个匿名管道,将前一个进程的结果发送给下一个进程。

匿名管道原理

上述代码中,利用管道进行进程间通信,虽然是利用文件描述符的形式进行读写,但实际上并没有创建实际的文件,并没有实际消耗磁盘空间。 实际上匿名管道是一个内核缓冲区,存储在内存之上。管道自动销毁的机制,是读写两端,也就是父子进程都关闭了文件描述符,操作系统为了避免资源浪费,自动的销毁了管道。

指令管道:

在这里插入图片描述
代码管道:
在这里插入图片描述
父子进程通关管道通信原理图

在这里插入图片描述
操作系统视角下的匿名管道父子进程通讯:
在这里插入图片描述

匿名管道读写

读写规则:

  1. 文件描述符设置为阻塞且管道为空时: read阻塞,进程阻塞,直到管道内有数据被输入。
  2. 文件描述符设置为非阻塞且管道为空时:read函数执行失败返回值为-1。
  3. 对端管道文件描述符关闭,read函数执行返回值为0。 如果管道内容还有内容,会将剩余的内容读完,并在下一次调用返回为0,因为本质上是read读到第一个文件结尾的标志位从而判断对端退出。
  4. 文件描述符设置为阻塞且管道为满时 :write阻塞,进程组设,直到管道内内容被取走。
  5. 文件描述符设置为非阻塞为满时:write函数执行失败返回值为-1.
  6. 对端管道文件描述符关闭,write产生SIGPIPE信号,可能导致write进程退出。
  7. 如果write写入管道的数据大于管道存储空间大小,则要写入的数据会分次写入管道,无法保证原子性。 如果write写入管道的数据小于管道存储空间大小,则要写入的数据会一次性写入管道,保证原子性。
  8. 进程退出,则管道释放。
  9. 匿名管道的通信被局限于有情缘关系的进程直接。
  10. 对于同一管道不能同时读写。

命名管道

命名管道使用

利用mkfifo函数(见补充说明部分)创建命名管道范例:

读取端:

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

int main()
{
    if(mkfifo("./testFileName", O_RDONLY) < 0 ) // 成功时返回0,失败时返回-1
    {
        perror("mkfifo error");
        exit(-1);
    }

    int input = open("./testFileName", O_RDONLY) ; 
    if(0 > input)
    {
        perror("open error");
        exit(-1) ;
    }

    char buf[1024];

    while(true)
    {
        buf[0] = 0;
        printf("Please wait...\n");
        ssize_t s = read(input, buf, sizeof(buf)-1);
        if(s > 0 )
        {
            buf[s-1] = 0;
            printf("client say# %s\n", buf);
        }
        else if(s == 0)
        {
            printf("client quit, exit now!\n");
            exit(1);
        }
        else
        {
            perror("read");
            exit(-1);
        }
    }

    close(input) ; 
    return 0  ;
}

写入端:

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

int main()
{
    int output = open("./testFileName", O_WRONLY);
    char buf[1024] ;

    while (true)
    {
        buf[0] = 0 ; 
        ssize_t s = read(0, buf, sizeof(buf)-1);
        if(s > 0)
        {
            if(0 > write(output, buf, sizeof buf - 1))
            {
                perror("write error\n");
                exit(-1);
            }
        }
        else
        {
            perror("read error");
            exit(-1);
        }
    }
    
    close(output);
    return 0 ; 
}

命名管道特性

O_NONBLOCK是文件描述符的属性
对于读操作:
当文件描述符属性设置为O_NONBLOCK时,读取操作立刻返回,无论是否有其他进程以写方式打开进程,如果管道文件中没有数据可读,那么读取操作将返回错误码,表示没有数据可读。 如果设置为非O_NONBLOCK时,阻塞到直到有相应的进程为写而打开该管道文件。
对于写操作:
当文件描述符被设置为O_NONBLOCK时,写操作立刻失败,并设置错误码。当文件描述符设置为非O_NONBLOCK时,阻塞到直达有进程为读操作而打开管道文件

共享内存

共享内存原理

共享内存本质是将要通讯的两个进程之间的进程地址空间映射到同一块物理内存中,共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。 共享内存的管理也是先描述再组织的,描述的结构体为shmid_ds结构体。

struct shmid_ds 结构体定义

struct shmid_ds 
{
    struct ipc_perm shm_perm;  // 共享内存的权限信息
    size_t shm_segsz;          // 共享内存的大小(字节)
    time_t shm_atime;          // 上次访问时间
    time_t shm_dtime;          // 上次分离时间
    time_t shm_ctime;          // 上次变更时间
    pid_t shm_cpid;            // 创建共享内存的进程ID
    pid_t shm_lpid;            // 最后一次操作共享内存的进程ID
    unsigned short shm_nattch; // 当前附加到共享内存的进程数
}

共享内存原理图
先上原理图

此外使用 ipcs -m指令可查看操作系统上所有的共享内端信息。
在这里插入图片描述

利用指令ipcrm -m + shmid 即可删除指定shmid的共享内存。

共享内存使用

利用共享内存实现server端和client端通信代码例子

代码逻辑:

./server —> ftok(依据路径和自定义ID生成key,key作为生成共享内存的参数) —>
shmet(生成共享内存) —> shmat(将共享内存与该进程绑定,得到用于使用共享内存的指针) —> 使用指针进行读取共享内存中的内容 ——> shmdt(解除共享内存和本进程的绑定) —>
shmctl(销毁共享内存) —> 结束

server先启动 —> client启动 —> shmget(只用IPC_CREAT选项,如果用了IPC_EXCL就会报错) —> 得到共享内存shmid —> shmat(将共享内存与该进程绑定,得到用于使用共享内存的指针) ——> 使用指针进行向共享内存中写入内容 shmdt(解除共享内存和本进程的绑定) —> 结束

#include "comm.hpp"

static int commShm(int size , int falgs)
{
    // ftok为生成进程通信键值的函数
    // 参数相同会生成一样的key  
  // 导致的效果就是两个进程调用该函数得到是同一块共享内存!
    // PROJ_ID/PATHNAME为宏定义
    key_t key = ftok(PATHNAME, PROJ_ID) ; 
    if(0 > key)
    {
        perror("ftok error");
        return -1 ;
    }

    int shmid ;
    //参数: key  共享内存大小  权限 


	//flage: IPC_CREAT(创建共享内存段)和 IPC_EXCL
	//(与 IPC_CREAT 一起使用,确保创建一个新的共享内存段)。
	//0666为共享内存权限
    if(0 > (shmid = shmget(key, size, falgs)) )
    {
        perror("shmget error");
        return -1 ; 
    }

    return shmid ;
}

int destroyShm(int shmid)
{
    if(shmctl(shmid, IPC_RMID, NULL) < 0)
    {
        perror("shmctl error\n"); 
        return -1 ;
    }

    printf("\n ipc destory success \n") ;
    return 0  ;
}


int createShm(int size)
{
    //IPC_CREAT表示如果共享内存不存在,则创建一个新的共享内存;IPC_EXCL表示如果共享内存已经存在,则创建失败;
    return commShm(size, IPC_CREAT | IPC_EXCL | 0666) ; 
}

int getShm(int size)
{
    return commShm(size, IPC_CREAT) ; 
}

client.cpp

#include "comm.hpp"

int main()
{
    int shmid  = getShm(1024);
    sleep(1); 
    char* addr = (char*)shmat(shmid, nullptr, 0);
    sleep(1);
    int i = 0 ; 
    while (i < 26)
    {
        addr[i] = 'A' + i ;  // 写入共享内存
        i++ ; 
        addr[i] = 0 ; 
        sleep(1);
    }

    shmdt(addr);
    sleep(2);

    return 0 ; 
}

server.cpp:

#include "comm.hpp"

int main()
{

    int shmid = createShm(1024) ; 
    sleep(1) ; 
    char* addr = (char*)shmat(shmid, nullptr, 0); 
    sleep(2) ;
    int i = 0 ; 
    while (i < 26)
    {
        sleep(1);
        printf("client %s\n", addr);
        i++ ;
    }

    shmdt(addr); 
    sleep(1);
    destroyShm(shmid) ; 
    return 0 ; 
    
}

特别提醒:

  1. 如果写关于使用共享内存进行进程通信代码,测试或使用时,如果没有用代码将创建的共享内存销毁,或是代码还没执行到共享内存销毁的代码就因为其他原因终止,下次启动该程序会失败,因为共享内存没有销毁,且用于生成的共享内存的key不变,执行到shmget()函数的时候就会出错。 解决方法为手动删除该共享内存。
  2. 因为是对内存进行访问,所以写端在写入时,对端关闭也不会造成影响。上述代码读端读取后,如果没有销毁共享内内存,内存中内容任然保留。

补充说明

mkfifo函数

mkfifo函数的原型如下:

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

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

功能:创建命名管道。

参数说明:
pathname:要创建的命名管道的路径名。
mode:创建的命名管道的权限,通常使用八进制表示。
mkfifo函数成功创建命名管道时返回0,失败时返回-1,并设置相应的错误码。创建的命名管道可以通过文件I/O函数进行读写操作。

shmdt
功能:将共享内存段与当前进程脱离
原型

 int shmdt(const void *shmaddr);

参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmat
功能:将共享内存段连接到进程地址空间
原型

 void *shmat(int shmid, const void *shmaddr, int shmflg);

参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

ftok
ftok函数是一个用于生成System V IPC(Inter-Process Communication,进程间通信)中的键值的函数。它的原型如下:

key_t ftok(const char *pathname, int proj_id);

该函数接受一个路径名和一个项目标识符作为参数,并返回一个唯一的键值。这个键值通常用于创建或访问共享内存、消息队列和信号量等System V IPC资源。

ftok函数的工作原理如下:

ftok函数通过将给定的路径名和项目标识符转换为一个32位的键值。
路径名必须指向一个现有的文件。ftok函数使用该文件的inode号和设备号来生成键值。
项目标识符是一个整数,用于区分不同的IPC资源。通常情况下,同一个路径名下的不同项目标识符会生成不同的键值。
需要注意的是,ftok函数在生成键值时可能会受到系统的限制。具体来说,它使用的是32位的键值,因此可能存在键值冲突的情况。如果生成的键值已经被使用,则可能导致创建或访问IPC资源失败。

另外,由于ftok函数使用了文件的inode号和设备号来生成键值,因此如果使用不同的文件系统或文件系统重新挂载,可能会导致生成的键值不同。

shmctl
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

在这里插入图片描述

shmid_ds

shmid_ds是System V共享内存的数据结构,用于描述和管理共享内存的属性和状态。它定义在<sys/shm.h>头文件中。

shmid_ds结构体的定义如下:

struct shmid_ds 
{
    struct ipc_perm shm_perm;  // 共享内存的权限信息
    size_t shm_segsz;          // 共享内存的大小(字节)
    time_t shm_atime;          // 上次访问时间
    time_t shm_dtime;          // 上次分离时间
    time_t shm_ctime;          // 上次变更时间
    pid_t shm_cpid;            // 创建共享内存的进程ID
    pid_t shm_lpid;            // 最后一次操作共享内存的进程ID
    unsigned short shm_nattch; // 当前附加到共享内存的进程数
    ...
};

shmid_ds结构体用于在共享内存的创建、访问和管理过程中记录共享内存的相关信息。通过操作shmid_ds结构体的成员,可以获取和修改共享内存的属性,例如权限、大小、创建者等信息。

需要注意的是,shmid_ds结构体中的其他成员没有在上述定义中列出,具体实现可能会有所差异。在使用shmid_ds结构体时,可以参考相关的系统文档和头文件中的定义。

POSIX IPC
POSIX IPC(Portable Operating System Interface Interprocess Communication)是一组跨平台的进程间通信机制,定义在POSIX标准中。它提供了一种可移植的方式来实现进程间的通信和同步。

POSIX IPC包括以下几种机制:

信号量(Semaphore):用于进程间的同步和互斥。通过使用信号量,进程可以等待某个事件的发生或者通知其他进程某个事件已经发生。

消息队列(Message Queue):用于进程间的异步通信。进程可以将消息发送到消息队列中,其他进程可以从队列中接收消息。

共享内存(Shared
Memory):允许多个进程共享同一块内存区域。这种机制可以提高进程间的数据传输效率,但需要进行适当的同步和互斥操作来保证数据的一致性。

互斥锁(Mutex):用于进程间的互斥访问共享资源。通过互斥锁,只有一个进程可以访问共享资源,其他进程需要等待锁的释放才能访问。

POSIX IPC提供了更高级别、更易用的进程间通信机制,相对于System V
IPC而言,它更加灵活、可移植,并且在现代操作系统中得到广泛支持。

System V IPC

System V IPC(进程间通信)是指System V Unix操作系统提供的一组机制,用于进程间通信。它包括三种主要类型的IPC:共享内存、消息队列和信号量。

共享内存允许多个进程访问同一内存段,实现进程间高效的数据共享。消息队列提供了进程发送和接收消息的方式,实现异步通信。信号量用于进程同步和协调,确保多个进程以受控的方式访问共享资源。

System V IPC提供了一种低级别的进程间通信接口,在类Unix操作系统中广泛使用。然而,它已经被较新的IPC机制(如POSIX IPC)所取代,这些机制提供了更好的功能和易用性。

文章来源:https://blog.csdn.net/WSK1454360679/article/details/134999364
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。