进程间通信

进程是需要频繁的和其他进程进行交流的。
在一些操作系统中,协作的进程可能共享一些彼此都能读写的公共资源。公共资源可能在内存中也可能在一个共享文件。

两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时,这种就被称为竞态条件(race condition)

不仅共享资源会造成竞态条件,事实上共享文件、共享内存也会造成竞态条件、那么该如何避免呢?或许一句话可以概括说明:禁止一个或多个进程在同一时刻对共享资源(包括共享内存、共享文件等)进行读写

把对共享内存进行访问的程序片段称作 临界区域(critical region)临界区(criticalsection)。如果我们能够正确的操作,使两个不同进程不可能同时处于临界区,就能避免竞争条件。
在这里插入图片描述

信号

信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有信号都是系统预定义的
信号跟信号量虽然名字相似,但两者用途完全不一样。

可以通过 kill -l 命令,查看所有的信号:
在这里插入图片描述
信号由谁产生?
1)由 shell 终端根据当前发生的错误(段错误、非法指令等)而产生相应的信号
比如:socket 通信或者管道通信,如果读端都已经关闭,执行写操作(或者发送数据),将导致执行写操作的进程收到 SIGPIPE 信号(表示管道破裂)
该信号的默认行为:终止该进程。
2) 在 shell 终端,使用killkillall命令产生信号
3)在程序代码中,调用kill系统调用产生信号

信号种类

SIGABORT		进程异常终止
SIGALRM 	    超时告警
SIGFPE 			浮点运算异常
SIGHUP 			连接挂断
SIGILL 		    非法指令
SIGINT 			终端中断  (Ctrl+C将产生该信号)
SIGKILL 		*终止进程                             
SIGPIPE 		向没有读进程的管道写数据
SIGQUIT 		终端退出(Ctrl+\将产生该信号)
SIGSEGV 		无效内存段访问
SIGTERM 		终止
SIGUSR1      	*用户自定义信号1
SIGUSR2 		*用户自定义信号2 

上面的信号若不被捕获,则进程接受后都会终止!

信号的捕获,是指定接受到某种信号后,去执行指定的函数。

SIGCHLD 		子进程已停止或退出
SIGCONT 	 	*让暂停的进程继续执行
SIGSTOP 		*停止执行(即“暂停")
SIGTSTP 		中断挂起
SIGTTIN 		后台进程尝试读操作
SIGTTOU 		后台进程尝试写

信号的处理方式:
1.忽略此信号(有两个信号是应用进程无法捕捉和忽略的,即 SIGKILLSEGSTOP,它们用于在任何时候中断或结束某一进程);
2.捕捉信号,指定信号处理函数进行处理;
3.执行系统默认动作,大多数都是终止进程

信号发送

信号的发送方式:
1.在 shell 终端用快捷键产生信号;
2.使用 killkillall 命令;
3.使用 kill 函数和 alarm 函数

kill()函数
作用:将信号发送给进程或进程组
头文件#include <signal.h>
函数原型int kill (pid_t pid,int signo)
返回值:成功:0;失败:-1,设置 errno (ID 非法,信号非法,普通用户杀 init 进程等权级问题)

函数参数:
signo参数:指定信号编号,不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
pid参数的四种情况:
pid > 0: 发送信号给指定的进程。
pid = 0: 发送信号给 与调用 kill 函数进程属于同一进程组的所有进程。
pid < 0: 取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。

进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组 ID 与进程组长 ID 相同。

权限保护:super 用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root 用户的 pid) 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 只能向自己创建的进程发送信号。
普通用户基本规则是:发送者实际或有效用户 ID == 接收者实际或有效用户 ID.

alarm() 函数
作用:设置定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 14号(SIGALRM) 信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一个定时器。
函数原型unsigned int alarm(unsigned int seconds);
返回值:0 或剩余的秒数,无失败。
常用:取消定时器 alarm(0),返回旧闹钟余下秒数。
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm 都计时。

信号捕捉

signal()函数
作用:注册一个信号捕捉函数

typedef void (*sighandler_t)(int);

函数原型:sighandler_t signal(int signum, sighandler_t handler);
函数参数:
signum:欲要设置捕捉的信号编号;
handler:回调函数,信号捕捉后所要执行的函数;

注意:该函数由 ANSI 定义,由于历史原因在不同版本的 Unix 和不同版本的 Linux 中可能有不同的行为。因此应该尽量避免使用它,取而代之使用 sigaction 函数。

sigaction() 函数
作用:修改(捕捉)信号处理动作(通常在 Linux 用其来注册一个信号的捕捉函数)
函数原型int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
返回值:成功:0;失败:-1,设置 errno
参数
signum:欲要设置捕捉的信号编号;
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。

struct sigaction 结构体
struct sigaction
{
	void (*sa_handler)(int);
	void (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t sa_mask;
	int sa_flags;
	void (*sa_restorer)(void);
};

sa_restorer:该元素是过时的,不应该使用,POSIX.1 标准将不指定该元素。(弃用)
sa_sigaction:当 sa_flags 被指定为 SA_SIGINFO 标志时,使用该信号处理程序。(很少使用)

重点掌握:
sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为 SIG_IGN 表忽略 或 SIG_DFL 表执行默认动作
sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sa_flags:通常设置为 0,表使用默认属性。

信号捕捉特性:

1、进程正常运行时,默认 PCB 中有一个信号屏蔽字,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由默认mask来指定。而是用 sa_mask 来指定。调用完信号处理函数,再恢复为默认mask。
2、XX 信号捕捉函数执行期间,XX 信号自动被屏蔽。
3、阻塞的常规信号不支持排队,产生多次只记录一次。(后 32 个实时信号支持排队)

Linux信号(signal)

信号集

什么是信号集?
信号集,用sigset_t类型表示,实质是一个无符号长整形。
用来表示包含多个信号的集合。

信号集操作原理
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。而我们可以在应用程序中自定义 set 来改变 mask,已达到屏蔽指定信号的目的。

信号集的基本操作

//sigemptyset      把信号集清空
//sigfillset       把所有已定义的信号填充到指定信号集
//sigdelset        从指定的信号集中删除指定的信号
//sigaddset        从指定的信号集中添加指定的信号

//sigismember   
//判断指定的信号是否在指定的信号集中
//如果是,    返回 1
//如果不是, 返回 0
//信号无效, 返回-1

进程的“信号屏蔽字”是一个信号集
想目标进程发送某信号时,如果这个信号在目标进程的信号屏蔽字中,则目标进程将不会捕获到该信号,即不会执行该信号的处理函数。
当该进程的信号屏蔽字不再包含该信号时,则会捕获这个早已收到的信号(执行对应的函数)

//修改进程的“信号屏蔽字”
//使用sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

//参数:
how:
    SIG_BLOCK    //把参数set中的信号添加到信号屏蔽字中
    SIG_UNBLOCK  //把参数set中的信号从信号屏蔽字中删除
    SIG_SETMASK  //把参数set中的信号设置为信号屏蔽字
set
	//传入参数,是一个位图,set 中哪位置 1,就表示当前进程屏蔽哪个信号。
oldset
   //返回原来的信号屏蔽字

当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,可通过sigpending函数获取这些已经发生了但是没有被处理的信号。

阻塞式等待信号
(1) pause:阻塞进程,直到发生任一信号后
(2) sigsuspend
用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。 即,只等待信号屏蔽字之外的信号
函数原型:int sigpending(sigset_t *set);
函数参数: set 传出参数。
返回值:成功:0;失败:-1,设置 errno

管道

对于匿名管道,它的通信范围是存在父子关系的进程。
因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。

shell 里面执行 A | B命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系,它俩的父进程都是 shell

匿名管道的生命周期随进程的创建而建立,随进程的结束而销毁

对于命名管道,它可以在不相关的进程间也能相互通信。
因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。

不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

消息队列

消息队列可以解决管道的通信方式效率低,不适合进程间频繁地交换数据的问题。
在这里插入图片描述
A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。

消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在。

消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。
在 Linux 内核中,会有两个宏定义 MSGMAXMSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。

消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

小林coding

信号量

在多进程并发执行时,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。

此指的“信号量”是指 System V IPC 的信号量,与线程所使用的信号量不同。该信号量,用于进程间通信。

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作
1.一个是 P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
2.另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

P操作、V操作都是原子操作,即其在执行时,不会被中断P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,两个操作是必须成对出现的。

信号初始化为 1,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存;
信号初始化为 0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。

共享内存

允许两个或多个进程(不相关或有亲缘关系)访问同一个逻辑内存的机制。它是共享和传递数据的一种非常有效的方式。
不同进程之间共享的内存通常安排为同一段物理内存。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。
在这里插入图片描述

共享内存实现进程间通信是进程间通信最快的

多个进程同时修改同一个共享内存,很有可能会发生冲突(共享内存并没有提供任何的保护机制,包括同步与互斥)。

两种常用共享内存方式:
System V版本的共享内存 shmm
1.多个进程直接共享内存

文件映射 mmap
1.文件进行频繁读写,将一个普通文件映射到内存中
2.将特殊文件进行匿名内存映射,为关联进程提供共享内存空间
3.为无关联的进程提供共享内存空间,将一个普通文件映射到内存中

共享内存函数

1.ftok函数生成key标识符

key_t ftok(const char *pathname,int proj_id)

2.创建一个共享内存块,返回这个共享内存块的标识符shmid

int shmget(key_t key,size_t size,int shmflg)
size //申请的共享内存的大小,为4k的整数倍;
shmflg //IPC_CREAT 创建新的共享内存,已存在 使用IPC_EXCL

3.挂接共享内存(将进程地址空间挂接到物理空间,可以有多个挂接)

void *shmat(int shmid, const void *shmaddr, int shmflg)
shmid  //挂接的共享内存ID.
shmaddr //一般为0,表示连接到由内核选择的第一个可用地址上
shmflg // 一般为0

4.取消共享内存映射

int shmdt(const void *shmaddr);

5.用于控制共享内存

int shmctl(int shmid, int cmd, struct shmid_ds  *buf);
shmid // 由shmget返回的共享内存标识码
cmd   // 将要采取的动作(可取值:IPC_STAT、IPC_SET、IPC_RMID)
buf   // 指向一个保存着共享内存的模式状态和访问权限的数据结构

进程间通信之共享内存

Socket

Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。

int socket(int domain, int type, int protocal)

domain 参数用来指定协议族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6 AF_LOCAL/AF_UNIX 用于本机;
type 参数用来指定通信特性,比如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字;
protocal 参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前一般写成 0 即可;

根据创建 socket 类型的不同,通信的方式也就不同:
实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket;

本地 socket 被用于在同一台主机上进程间通信的场景:
本地 socket 的编程接口和 IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议;
本地 socket 的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报 socket 实现;

本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。

  • 34
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞大圣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值