Binder总结(一)—— 源起

本文详细介绍了Linux系统中多种进程间通信(IPC)机制,包括无名管道、有名管道(FIFO)、信号、消息队列、共享内存以及Socket。着重阐述了每种机制的工作原理、使用场景和优缺点,如管道的单工和半双工特性,信号的即时性、可延迟和可阻塞,消息队列的数据持久性和顺序性,共享内存的高速通信以及Socket的网络通信能力。通过对这些机制的理解,读者可以更好地掌握进程间通信的实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【前言】

Android系统基于linux kernel实现,Linux已经提供了那么多IPC方式,为何要新起Binder?

Linux IPC方式起底:

1. 管道

无名 —— 普通管道pipe: 通常有两种限制:一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将自动退出
无名 —— 流管道s_pipe::去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输。


有名 —— 命名管道:name_pipe(FIFO):去除了第二种限制,可以在许多并不相关的进程之间进行通讯。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。

1.1 管道实现

管道借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。

管道写函数:通过将字节复制到 VFS 索引节点指向的物理内存而写入数据。

管道读函数:通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号

PS: 管道和FIFO是相反的,进程向其中写消息时,管道和FIFO必需已经打开来读,那么内核会产生SIGPIPE信号。

满足如下条件时,才能进行实际的内存复制工作:

1. 内存中有足够的空间可容纳所有要写入的数据;
2. 内存没有被读程序锁定。
如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。

char *FIFO = "/tmp/my_fifo";
unlink(FIFO);
mkfifo(FIFO,0666);

int fd;
fd = open (FIFO,O_WRONLY);
write(fd,s,sizeof(s));
fd= open(FIFO,O_RDONLY);  
read(fd,buffer,80);

close(fd);

2. 信号

用于进程间通信外,进程还可以发送信号给进程本身,还有三可:

  • 可随时:信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态
  • 可延迟:如果该进程当前并未处于执行状态,则该信号就有内核保存起来,直到该进程恢复执行并传递给它为止。
  • 可阻塞:如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时信号才被传递给进程。

Linux系统中常用信号:
(1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
(2)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。
(3)SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\\键将产生该信号。
(4)SIGBUS和SIGSEGV:进程访问非法地址。
(5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
(6)SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。
(7)SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。
(8)SIGALRM:定时器信号。
(9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。

信号事件主要有两个来源:

  • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件来源:终止进程信号、其他进程调用kill函数、软件异常产生信号。

信号处理框架:

 每个进程都有PCB,PCM设计了一个signal的bitmap,其中的每位与具体的signal相对应,这与中断机制是保持一致的。当系统中一个进程A通过signal系统调用向进程B发送signal时,设置进程B的对应signal bitmap,类似于触发了signal对应中断。

signal的执行点:内核态返回用户态时,在返回时,如果发现待执行进程存在被触发的signal,那么在离开内核态之后(也就是将CPU切换到用户模式),执行用户进程为该signal绑定的signal处理函数,从这一点上看,signal处理函数是在用户进程上下文中执行的。当执行完signal处理函数之后,再返回到用户进程被中断或者system call(软中断或者指令陷阱)打断的地方。

 Signal机制实现的比较灵活,用户进程由于中断或者system call陷入内核之后,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态),将断点信息都保存到了堆栈中,在内核返回用户态时,如果存在被触发的signal,那么直接将待执行的signal处理函数push到堆栈中,在CPU切换到用户模式之后,直接pop堆栈就可以执行signal处理函数并且返回到用户进程了。Signal处理函数应用了进程上下文,并且应用实际的中断模拟了进程的软中断过程。

signal(SIGALRM,when_alarm); //当接收到SIGALRM信号时,调用when_alarm函数  

3. 消息队列

一个存放消息(数据)容器。将消息写入消息队列,然后再从消息队列中取消息,一般来说是先进先出的顺序。可以解决两个进程的读写速度不同系统耦合等问题,而且消息队列里的消息哪怕进程崩溃了也不会消失

#include <mqueue.h>

struct mq_attr  {
  long int mq_flags;    消息队列的标志:0或O_NONBLOCK,用来表示是否阻塞
  long int mq_maxmsg;   消息队列的最大消息数
  long int mq_msgsize;  消息队列中每个消息的最大字节数
  long int mq_curmsgs;  消息队列中当前的消息数目
  long int __pad[4];  
};

mqd_t mqID = mq_open("/anonymQueue", O_RDWR | O_CREAT, 0666, NULL);   用于打开或创建一个消息队列
mq_unlink("/anonymQueue"); 

mq_attr mqAttr;
mq_getattr(mqID, &mqAttr)   用于获取当前消息队列的属性
char *buf = new char[mqAttr.mq_msgsize];  

char msg[] = "yuki";
mq_receive(mqID, buf, mqAttr.mq_msgsize, NULL)    接收信息
mq_send(mqID, msg, sizeof(msg), i)                发送消息



POSIX消息队列大小的限制:ulimit -a |grep message 
:mq_maxmsg = 10 
:mq_msgsize = 8192

修改大小限制:ulimit -q 1024000000

MQ_OPEN_MAX:一个进程能同时打开的消息队列的最大数目,POSIX要求至少为8;
MQ_PRIO_MAX:消息的最大优先级,POSIX要求至少为32;

4. 共享内存

共享内存也是一种IPC,它是目前可用IPC中最快的,它是使用方式是将同一个内存区映射到共享它的不同进程的地址空间中,这样这些进程间的通信就不再需要通过内核,只需对该共享的内存区域进程操作就可以了,和其他IPC不同的是,共享内存的使用需要用户自己进行同步操作

mmap函数主要的功能就是将文件或设备映射到调用进程的地址空间中,当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用read,write等系统调用。在很大程度上提高了系统的效率和代码的简洁性。

mmap函数

  • 对普通文件提供内存映射I/O,可以提供无亲缘进程间的通信;
  • 提供匿名内存映射,以供亲缘进程间进行通信。
  • 对shm_open创建的POSIX共享内存区对象进程内存映射,以供无亲缘进程间进行通信。
#include <sys/mman.h>
在实际的使用过程要考虑到进程间的同步,通常会用信号量来进行共享内存的同步。

void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);

start:指定描述符fd应被映射到的进程地址空间内的起始地址,它通常被设置为空指针NULL:内核自动选择起始地址,该函数的返回值即为fd映射到内存区的起始地址。

len:映射到进程地址空间的字节数,它从被映射文件开头的第offset个字节处开始,offset通常被设置为0。

prot:
   PROT_READ:数据可读;
   PROT_WRITE:数据可写;
   PROT_EXEC:数据可执行;
   PROT_NONE:数据不可访问;

flags:设置内存映射区的类型标志。内核的虚拟内存算法会保持内存映射文件和内存映射区的同步
   MAP_SHARED:调用进程对被映射内存区的数据所做的修改对于共享该内存区的所有进程都可见,而且确实改变其底层的支撑对象(一个文件对象或是一个共享内存区对象)。
   MAP_PRIVATE:调用进程对被映射内存区的数据所做的修改只对该进程可见,而不改变其底层支撑对象。
   MAP_FIXED:该标志表示准确的解释start参数,一般不建议使用该标志,对于可移植的代码,应该把start参数置为NULL,且不指定MAP_FIXED标志。

fd:有效的文件描述符。

len:映射到进程地址空间的字节数,它从被映射文件开头的第offset个字节处开始,offset通常被设置为0。

offset:相对文件的起始偏移。

//mmap失败返回值:MAP_FAILED

mmap成功后,可以关闭fd,一般也是这么做的,这对该内存映射没有任何影响。

//解除mmap
int munmap(void *start, size_t len);  //成功返回0,出错返回-1  
start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
len:映射区的大小。

//希望硬盘上的文件内容和内存映射区中的内容实时一致
int msync(void *start, size_t len, int flags);  //成功返回0,出错返回-1  
start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
len:映射区的大小。
flags:同步标志
    MS_ASYNC:异步写,一旦写操作由内核排入队列,就立刻返回;
    MS_SYNC:同步写,要等到写操作完成后才返回。
    MS_INVALIDATE:使该文件的其他内存映射的副本全部失效。

mmap原理:内核怎样保证各个进程寻址到同一个共享内存区域的内存页面


struct page {
    union {
        struct address_space *mapping;
    }
}

1、 物理页面驻留位置page cache & swap cache:

一个被访问文件的物理页面都驻留在page cache或swap cache中, page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

2、文件打开fd与 address_space结构的对应:

一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个PageFault。

4、 对于共享内存映射情况,缺页异常处理程序:

a. 首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页)。

如果找到,则直接返回地址;

如果没有找到,则判断该页是否在交换区 (swap area),如果在,则执行一个换入操作;

如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。 
PS:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页 面,并返回相应地址,同时,进程页表也会更新。

5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。 
 

5. Socket

Socket是应用层和传输层之间的桥梁,基于TCP/IP 四层协议(OSI 7层),既可以在本地单机上进行,也可以跨网络进行。

套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。

(1)套接字的域
AF_INET,它指的是Internet网络,用到服务器计算机的IP地址和端口
AF_UNIX(AF_LOCAL),表示UNIX文件系统,它的地址就是文件名。

(2)套接字的端口号
端口号是一个16位无符号整数,范围是0-65535,低于256的端口号保留给标准应用程序,
比如pop3的端口号就是110。

(3)套接字协议类型
a.流套接字(SOCK_STREAM):
在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是
一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到
达,而且它还有一定的出错后重新发送的机制。

b.数据报套接字(SOCK_DGRAM):
不需要建立连接和维持一个连接,通过UDP/IP协议实现的,对可以发送的数据的长度有
限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不
是一个可靠的协议,但是它的速度比较高,因为它并不需要总是要建立和维持一个连接。

c.原始套接字(SOCK_RAW):
我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不
能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大
程度上依赖于SOCKET_RAW。

三者区别:
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,
数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用
原始套接字。
#include <sys/socket.h>

// 1.socket()
int socket(int domain, int type, int protocol);

domain:即协议域,又称为协议族(family),有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等。对于BSD,是AF,对于POSIX是PF

type:指定socket类型。常用的socket类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等

protocol:就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等

//2. binder()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 上个函数返回值
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
addrlen:对应的是地址的长度。

struct sockaddr_un srv_addr; //server Unix域
struct sockaddr_un clt_addr; //client Unix域

//3. accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
监听指定的socket地址

//4. 读写函数
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()


//1. create socket to bind local IP and PORT
    int lsn_fd = socket(AF_UNIX, SOCK_STREAM, 0); // PF_UNIX  SOCK_STREAM, 0

//2. create local IP and PORT
    srv_addr.sun_family = AF_UNIX;
    strncpy(srv_addr.sun_path, UNIX_DOMAIN, sizeof(srv_addr.sun_path) - 1);
    unlink(UNIX_DOMAIN);

//3. bind sockfd and sockaddr
  ret = bind(lsn_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));

//4. listen lsn_fd, try listen 1:第二个参数为相应socket可以排队的最大连接个数
  ret = listen(lsn_fd, 1);

//5. accept socket_fd 监听
    clt_len = sizeof(clt_addr);
    while(1) {
    apt_fd = accept(lsn_fd, (struct sockaddr*)&clt_addr, &clt_len);

//6.read()、write()等函数
int snd_num = write(apt_fd, send_buf, 1024);
int rcv_num = read(apt_fd, recv_buf, sizeof(recv_buf));

//7. client connect to server
  ret = connect(connect_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));

 PS:

TCP/IP四层模型:

1.链路层(数据链路层/网络接口层):包括操作系统中的设备驱动程序、计算机中对应的网络接口卡

2.网络层(互联网层):处理分组在网络中的活动,比如分组的选路。

3.运输层:主要为两台主机上的应用提供端到端的通信。

4.应用层:负责处理特定的应用程序细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值