003_音频开发_基础篇_Linux进程通信(20种你了解几种?)
文章目录
- 003_音频开发_基础篇_Linux进程通信(20种你了解几种?)
- 创作背景
- `Linux` 进程通信类型
- `fork()` 函数
- `fork()` 输出 `2` 次
- `fork()` 输出 `8` 次
- `fork()` 返回值
- `fork()` 创建子进程 方式一
- `fork()` 创建子进程 方式二
- `fork()` 创建子进程 方式三
- `fork()` 创建子进程 方式四
- `fork()` 复制父进程上下文
- `Socket` 套接字
- `Socket` - (`Linux IP` 协议)
- 总体架构
- `IP 协议` 协议重要结构体
- `IP 协议 API函数`
- `IP 协议族`
- `IP 协议 socket` 类型
- `IP 协议` 操作函数
- `Socket` - `UDS` (待更新)
- `IP 协议` VS. `UDS`
- `UDS` 应用场景 —— `Docker Daemon`
- `UDS` 应用场景 —— `Azure IoT Edge`
- Socket Activation
- 管道 `pipe`
- 匿名管道 `Anonymous pipes`
- 命名管道 `Named pipe/FIFOs`
- 消息队列
- 共享内存
- 信号量
- 文件锁
- 共享文件
- RPC (Remote Procedure Call)
- Protocol Buffer
- gRPC
- RabbitQM
- ZeroMQ
- RPC over HTTP
- FlatBuffers
创作背景
学历代表过去、能力代表现在、学习力代表将来。 一个良好的学习方法是通过输出来倒逼自己输入。写博客既是对过去零散知识点的总结和复盘,也是参加了 零声教育 写博客活动。
零声教育体验课:https://xxetb.xetslk.com/s/3fbO81
本文是开发过程中的知识点总结,供大家学习交流使用,如有任何错误或不足之处,请在评论区指出。
Linux
进程通信类型
如下表描述了Linux
常用进程间通信方式了,包含了多种技术,例如基于系统调用、套接字、共享内存和消息队列等,用于实现进程之间的数据传输、同步和协作,以满足不同应用场景下的需求。
序号 | 中文名 | 英文名 | 描述 |
---|---|---|---|
1 | 管道 | Pipe | 匿名管道 在父子进程或兄弟进程之间进行通信。 命名管道 独立进程间通信 |
2 | 消息队列 | Message Queues | 进程之间通过消息队列进行通信,可以实现异步通信。 |
3 | 共享内存 | Shared Memory | 进程可以通过映射共享内存区域来实现共享数据,提高通信效率。 |
4 | 信号 | Signals | 进程可以通过信号来通知其他进程发生了某些事件。 |
5 | 信号量 | Semaphores | 用于控制对共享资源的访问,防止多个进程同时访问造成冲突。 |
6 | 文件锁 | File Locks | 进程可以通过文件锁来实现对共享文件的互斥访问。 |
7 | 共享文件 | Shared Files | 进程可以通过读写共享文件来进行通信,但需要注意同步和并发访问的问题。 |
8 | Memory Mapped Files | Memory Mapped Files | 进程可以将文件映射到它们的地址空间中,实现对文件内容的共享访问。 |
9 | 直接操作内存 | Direct Memory Access | 进程可以直接读写其他进程的内存来进行通信,但这通常需要特殊的权限和保护机制。 |
10 | Event Loops | Event Loops | 进程可以使用事件循环来监听和处理事件,实现进程间的消息传递和通知。 |
11 | 套接字/UDS | Unix | Unix域套接字(Unix Domain Socket),用于本地进程间通信。 |
12 | DBus | dbus | D-Bus 是一个进程间通信机制,常用于 Linux 桌面环境中实现进程间通信和协作。 |
13 | 套接字/IP | IP | Internet套接字(Internet Domain Socket),用于在网络中进行通信。 |
14 | Remote Procedure Calls | RPC | 允许进程调用另一个进程中的函数,实现远程通信和协作。 |
15 | RPC over HTTP | RPC over HTTP | 使用HTTP协议作为传输层,通过远程过程调用(RPC)来进行进程间通信。 |
16 | ZeroMQ | ZeroMQ | ZeroMQ 是一个开源的消息队列库,提供了丰富的 API 和通信模式,用于实现进程间通信。 |
17 | Socket Activation | Socket Activation | systemd中的一种机制,允许在需要时才启动服务进程。通过UNIX域套接字传递连接。 |
18 | RabbitMQ | RabbitMQ | 基于AMQP协议的开源消息代理,用于实现可靠的异步通信。 |
19 | gRPC | gRPC | gRPC 是一个高性能、开源的远程过程调用(RPC)框架,可以用于实现进程间通信。 |
20 | Protocol Buffers | Protocol Buffers | 使用序列化格式将数据结构编码后进行传输,实现进程间通信。 |
21 | FlatBuffers | FlatBuffers | 使用序列化格式将数据结构编码后进行传输,实现进程间通信。 |
fork()
函数
fork()
是一个系统调用,用于创建一个与当前进程完全相同的新进程。- 原进程称为 父进程,新创建的进程称为 子进程。
- 在父进程中,
fork()
返回子进程的进程ID
;在子进程中,fork()
返回0
。 - 在调用
fork()
时,两个内存空间具有相同的内容。 - 一个进程进行的内存写操作、文件映射和解除映射操作不会影响另一个进程。
- 注意:
fork()
调用会有2
次返回,正常我们的函数调用只有一次返回。
fork()
输出 2
次
fork()
函数调用后输出两次:当前进程 + 子进程
int main(void)
{printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());fork();printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());return 0;
}
fork()
输出 8
次
int main(void)
{printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());fork(); fork(); fork();printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());return 0;
}
fork()
返回值
fork()
函数在父进程中返回子进程的进程 ID
,而在子进程中返回 0
。调用失败,它将返回一个负值,通常是 -1
,表示创建新进程失败。
int main(void)
{printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());pid_t pid = fork();if (0 == pid){printf("child pid=%d, parent pid=%d\n", getpid(), getppid());}else if (0 < pid){printf("pid=%d, parent pid=%d\n", getpid(), getppid());}else{printf("fork() failed.\n");}printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());return 0;
}
fork()
创建子进程 方式一
父进程创建 2
个子进程
int main()
{printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());// 1. parentpid_t id = fork();if (id == 0) // 2. new child{fork(); // 3. return twice (one is new child)}// total: 3 thread = 1 current + 2 childprintf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());return 0;
}
fork()
创建子进程 方式二
父进程创建 4
个子进程
int main()
{printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid()); // 1. parentpid_t id = fork();if (id == 0) // 2. new child{id = fork();if (id == 0) // 3. new child{id = fork();if (id == 0) // 4. new child{fork(); // 5. return twice (one is new child)}}} // total: 3 thread = 1 parent + 4 child printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());return 0;
}
fork()
创建子进程 方式三
父进程创建 4
个子进程
int main(void) {printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid()); pid_t id[4]; // 1. parentfor (int i = 0; i < 4; i++){id[i] = fork();if (id[i] == 0) // 2. new 4 child, by the same parent.{printf("\tI'm child pid=%d, parent pid=%d\n", getpid(), getppid());break;}else if (id[i] > 0){printf("\tI'm parent pid=%d, parent pid=%d\n", getpid(), getppid());}else{printf("fork() failed.\n");}} // total: 5 thread = 1 parent + 4 child printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid()); // print 5 timesreturn 0;
}
fork()
创建子进程 方式四
父进程比子进程先退出,子进程会变为“孤儿进程”,被1
号/init --user
进程收养,进程编号也会变为 1
。
int main()
{printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());pid_t id[4];// 1. parentfor (int i = 0; i < 4; i++){id[i] = fork();if (id[i] > 0) // 2. new 4 child, child create child.{break;}else if (id[i] == 0) // child exit{}else //failed {}}// total: 5 thread = 1 parent + 4 child printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());return 0;
}
fork()
复制父进程上下文
char buffer[64] = "hello world!";
int main()
{printf("#### Start pid=%d, parent pid=%d\n\n", getpid(), getppid());pid_t pid = fork();if (0 < pid) // parent{printf("\tI'm parent pid=%d, parent pid=%d\n", getpid(), getppid());for (int i = 0; i < 10; i++){sleep(1);printf("\t%d)Parent (%s)\n", i, buffer);}}else if (0 == pid) // child{printf("\tI'm child pid=%d, parent pid=%d\n", getpid(), getppid());for (int i = 0; i < 5; i++){if (1 == i) // child thread modify buffer {strcpy(buffer, "Sub message.");}sleep(1);printf("\t%d)Sub (%s)\n", i, buffer);}}else{printf("fork() failed.\n");}printf("#### Exit pid=%d, parent pid=%d\n", getpid(), getppid());return 0;
}
Socket
套接字
-
套接字是一种软件对象,作为端点,建立了服务器端和客户端程序之间的双向网络通信链路。
-
套接字允许在同一台或不同的机器上两个不同进程之间进行通信。
-
Socket
本意是 插座/插口 的意思,其实也可以这样理解,一台电脑(服务器)有很多插口,另一台电脑(客户端)有很多插头,把客户端电脑的一个插头,插入到服务端电脑的插孔种,这样2
台电脑就建立了通信桥梁。 -
下图左边是 服务端 创建
socket
流程,右边是 客户端 创建流程:
Socket
- (Linux IP
协议)
总体架构
glibc:
glibc
提供对系统调用的封装,可以使用标准C
函数来进行 系统调用,而无需直接与系统调用接口打交道。glibc(GNU C Library)
是GNU
项目中的一个重要组件,是C
语言编程中常用的标准C
库。- 系统调用 是操作系统提供给用户空间程序与内核进行交互的接口。当用户空间程序需要操作硬件设备、访问文件系统、进行网络通信等底层操作时,通常需要通过系统调用来实现。
INET:
提供了各种网络协议函数和接口,使用户空间程序能够通过系统调用来进行网络通信。INET
相关的文件通常位于/proc/net/
目录下,提供了关于网络状态、连接信息、路由表等的统计数据和配置信息。VFS/virtual File System:
虚拟文件系统。操作系统中的一种抽象概念,用于管理和访问不同类型的文件系统,使用户和应用程序能够以统一的方式访问文件,而无需了解底层文件系统的细节。ARP/Address Resolution Protocol:
地址解析协议。ARP
是用于将IP
地址映射到物理硬件地址(如MAC
地址)的协议。当计算机在本地网络中需要发送数据到另一台计算机时,它需要知道目标计算机的MAC
地址。ARP
协议通过在本地网络上广播ARP
请求来获取目标IP
地址的MAC
地址,然后将这个映射关系保存在ARP
缓存中,以便后续通信中使用。ICMP/Internet Control Message Protocol:
是因特网控制报文协议, 不需要指定端口。用于在IP
网络上发送控制消息的协议,用于检测网络连接是否正常、诊断网络问题以及报告错误。还用于ping
命令,用于测试与另一个主机之间的网络连接是否正常。
IP 协议
协议重要结构体
state:
表示套接字当前所处的状态,如连接已建立、连接中等。type:
套接字类型,表示套接字的通信方式,如常用的 流式套接字、数据报套接字等。flags:
套接字的标志,用于指示特定的套接字属性或配置。ops:
指向协议特定套接字操作的指针,通常指向一组函数指针,用于执行套接字操作,如发送数据、接收数据等。file:
这是指向套接字相关文件的指针。在Linux
内核中,套接字也可以通过文件系统进行访问和操作,这个指针可能指向套接字关联的文件结构。sk:
这是一个指向内部网络协议无关套接字表示的指针。它通常指向struct sock
类型的数据结构,该结构包含了更底层的网络协议相关信息。wq:
等待队列,用于处理各种套接字相关的等待事件,比如等待连接、等待数据等。
IP 协议 API函数
socket()
: 创建新套接字。bind()
: 将套接字与地址绑定。listen()
: 将套接字设置为监听模式。accept()
: 接受连接请求并创建新套接字。connect()
: 建立与目标套接字的连接。send()
: 发送数据到连接的套接字。recv()
: 从套接字接收数据。close()
: 关闭套接字。shutdown()
: 关闭套接字的读、写或两者。getsockopt()
: 获取套接字选项。setsockopt()
: 设置套接字选项。
IP 协议族
- 地址族和协议族其实是一样的,值也一样,都是用来识别不同协议的,为什么要搞两套东西呢?
- 这是因为之前
Unix
有两种风格系统:BSD
系统和POSIX
系统,对于BSD
系统,一直用的是AF
,对于POSIX
系统,一直用的是PF
。Linux
作为后起之秀,为了兼容,所以两种都支持,这样两种风格的Unix
下的软件就可以在Linux
上运行了。 POSIX
系统:Bell
实验室的Ken Thompson
和Dennis Richie
一起发明了Unix
系统。BSD
系统:BSD
代表伯克利软件条件,是20
世界70
年代,加州大学伯克利分校对贝尔实验室Unix
进行了一系列修改后的版本,它最终发展成了一个完整的操作系统,有着自己的一套标准。
IP 协议 socket
类型
/* Types of sockets. */
enum __socket_type
{SOCK_STREAM = 1, /* Sequenced, reliable, connection-based byte streams. */SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams of fixed maximum length. */SOCK_RAW = 3, /* Raw protocol interface. */SOCK_RDM = 4, /* Reliably-delivered messages. */SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based, datagrams of fixed maximum length. */SOCK_DCCP = 6, /* Datagram Congestion Control Protocol. */SOCK_PACKET = 10, /* Linux specific way of getting packets at the dev level. For writing rarp and other similar things on the user level. *//* Flags to be ORed into the type parameter of socket and socketpair and used for the flags parameter of paccept. */SOCK_CLOEXEC = 02000000, /* Atomically set close-on-exec flag for the new descriptor(s). */SOCK_NONBLOCK = 00004000 /* Atomically mark descriptor(s) as non-blocking. */
};
SOCK_STREAM
: 顺序、可靠、基于连接的字节流,通常用于TCP协议。SOCK_DGRAM
: 无连接、不可靠的固定最大长度数据报,通常用于UDP协议。SOCK_RAW
: 原始协议接口,允许直接访问网络层以下的协议。SOCK_RDM
: 可靠交付的消息,不保证顺序。SOCK_SEQPACKET
: 顺序、可靠、基于连接的固定最大长度数据报,每个数据报保持连续性。SOCK_DCCP
: 用于Datagram Congestion Control Protocol(DCCP)
的套接字类型。SOCK_PACKET
:Linux
特定的获取数据包的方式,用于在用户级别进行类似RARP
等操作。SOCK_CLOEXEC
: 用于socket
和socketpair
的标志,用于原子性地设置新描述符的close-on-exec
标志。SOCK_NONBLOCK
: 用于socket
和socketpair
的标志,用于原子性地将描述符标记为非阻塞。
IP 协议
操作函数
应用层相应的函数,通过 glibc
封装的函数,进行系统调用,最后调用这里的函数。
struct proto_ops {int family;struct module *owner;int (*release) (struct socket *sock);int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);int (*socketpair) (struct socket *sock1, struct socket *sock2);int (*accept) (struct socket *sock, struct socket *newsock, int flags, bool kern);int (*getname) (struct socket *sock, struct sockaddr *addr, int peer);__poll_t (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait);int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
#ifdef CONFIG_COMPATint (*compat_ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
#endifint (*gettstamp) (struct socket *sock, void __user *userstamp, bool timeval, bool time32);int (*listen) (struct socket *sock, int len);int (*shutdown) (struct socket *sock, int flags);int (*setsockopt) (struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen);int (*getsockopt) (struct socket *sock, int level, int optname, char __user *optval, int __user *optlen);
#ifdef CONFIG_COMPATint (*compat_setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen);int (*compat_getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen);
#endifint (*sendmsg) (struct socket *sock, struct msghdr *m, size_t total_len);/* Notes for implementing recvmsg: msg->msg_namelen should get updated by the recvmsg handlers iff msg_name != NULL. * It is by default 0 to prevent returning uninitialized memory to user space. The recvfrom handlers can assume that msg.msg_name is either * NULL or has a minimum size of sizeof(struct sockaddr_storage).*/int (*recvmsg) (struct socket *sock, struct msghdr *m, size_t total_len, int flags);int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma);ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags);ssize_t (*splice_read) (struct socket *sock, loff_t *ppos, struct pipe_inode_info *pipe, size_t len, unsigned int flags);int (*set_peek_off) (struct sock *sk, int val);int (*peek_len) (struct socket *sock);/* The following functions are called internally by kernel with sock lock already held. */int (*read_sock) (struct sock *sk, read_descriptor_t *desc, sk_read_actor_t recv_actor);int (*sendpage_locked)(struct sock *sk, struct page *page, int offset, size_t size, int flags);int (*sendmsg_locked) (struct sock *sk, struct msghdr *msg, size_t size);int (*set_rcvlowat) (struct sock *sk, int val);
};
- 如下是简易调用流程图: