进程间通信


1. 信号


1.1 信号是什么?


信号(signal),又称为软中断信号,用来通知进程发生了异步事件。进程之间可以互相发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知进程发生了什么事件,并不给该进程传递任何数据。

Linux中常用的信号:

信号名称信号描述
SIGALRM在用alarm()函数设置计数器超时时,产生此信号
SIGINT当用户按中断键(Ctrl+c)时,终端产生此信号并发送给前台进程组的进程
SIGKILL这是两个不能捕捉、忽略的信号之一,它提供了一种可以杀死任一进程的方法
SIGSTOP这是两个不能捕捉、忽略的信号之一,用于停止一个进程。
SIGPIPE如果在写管道时读进程已经终止,则产生该信号。
SIGTERM这是由kill命令发送的默认信号
SIGCHLD在一个进程终止或者停止时,将SIGCHLD信号发送给父进程。
SIGABRT调用abort()函数式产生此信号,进程异常终止
SIGSEGV该信号指示进程进行了一次无效的内存引用
SIGUSR1,SIGUSR2这是用户定义的两个信号,可以用于应用程序间进行通信

举例:
Linux可以使用kill命令向指定进程发送指定信号,kill命令的用法如下:

kill [选项]PID

kill命令可以支持-l列出系统支持的所有信号,如图:
在这里插入图片描述


1.2 信号函数


1.2.1 sigaction函数


Linux系统为大部分信号定义了缺省处理方法,当信号的缺省处理方法不满足需求时,可通过sigaction()函数进行改变。sigaction()函数的原型如下:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

sigaction()函数成功返回0,否则返回-1。

参数signum指出需要改变处理方法的信号,如SIGINT信号,但SIGKILL和SIGSTOP这两个信号是不可捕捉的。

参数act和oldact是一个sigaction结构体的指针,act为要设置的对信号的新处理方式,而oldact为原来对信号的处理方式。sigaction结构体定义在signal.h头文件中,它的定义如下:

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_handler是一个函数指针,用来指定信号发生时调用的信号处理函数;
  • sa_sigaction则是另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息,当sa_flags成员的值包含了SA_SIGINFO标志时,系统将使用sa_sigaction 函数作为信号处理函数,否则使用sa_handler作为信号处理函数;
  • sa_mask成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。可以使用sigemptyset()、sigaddset()、sigdelset()分别对这个信号集进行清空、增加屏蔽信号、删除屏蔽信号等操作;
  • sa_flags成员用于指定信号处理的行为,它可以是以下值的“按位或”组合:
      1. SA_RESTART:使被信号打断的系统调用自动重新发起;
      2. SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号;
      3. SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵尸进程;
      4. SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号;
      5. SA_RESETHAND:信号处理之后重新设置为默认的处理方式;
      6. SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数。
  • re_restorer成员则是一个已经废弃的数据域,不要使用。

1.2.2 kill函数


kill()函数用来向指定的进程发送一个指定的信号,kill()函数的原型如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

kill()函数成功返回0,否则返回-1。参数pid为发送sig信号的进程PID,参数sig为发送的信号。

程序清单10演示了kill()函数的用法,父进程向子进程发生SIGINT信号,并使用wait()函数获取子进程的退出状态。


2. 进程间通信


2.1 共享内存


共享内存概述
共享内存是允许两个不相关的进程访问同一个逻辑内存的进程间通信方法,是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。

不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言malloc()分配的内存一样。两个进程使用共享内存通信机制如下图所示。
在这里插入图片描述


POSIX共享内存区涉及四个主要步骤:

  • 指定一个名字参数调用shm_open,以创建一个新的共享内存区对象(或打开一个以存在的共享内存区对象);
  • 调用mmap把这个共享内存区映射到调用进程的地址空间;
  • 调用munmap() 取消共享内存映射;
  • 调用shm_unlink()函数删除共享内存段。

注意:在编译POSIX共享内存应用程序时需要加上-lrt参数。


打开或创建一个共享内存区
shm_open()函数用来打开或者创建一个共享内存区,两个进程可以通过给shm_open()函数传递相同的名字以达到操作同一共享内存的目的。它的原型如下:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open(const char *name, int oflag, mode_t mode);

函数成功返回创建或打开的共享内存描述符,与文件描述符相同作用,供后续操作使用,失败返回-1。

  • 参数name为指定创建的共享内存的名称,其它进程可以根据这个名称来打开共享内存;
  • 参数oflag 为以下值的或值:
      1. O_RDONLY:共享内存以只读方式打开;
      2. O_RDWR:共享内存以可读写方式打开;
      3. O_CREAT:共享内存不存在才创建;
      4. O_EXCL:如果指定了O_CREAT,但共享内存已经存在时返回错误;
      5. O_TRUNC:如果共享内存已存在则将大小设置为0;
  • 参数mode只有指定O_CREAT才有效指出,指出共享内存的权限,与open()函数类似。
    注意:新创建或打开的共享内存大小默认为0,需要设置大小才能使用。

删除共享内存
当使用完共享内存后,需要将其删除,以便释放系统资源,可通过shm_unlink()函数完成。shm_unlink()函数原型如下:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_unlink(const char *name);

函数成功返回0,否则返回-1。参数name为共享内存的名字。

设置共享内存大小
创建一个共享内存后,默认大小为0,所以需要设置共享内存大小。ftruncate()函数可用来调整文件或者共享内存的大小,它的原型如下:

#include <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t length);

函数成功返回0,失败返回-1。

参数fd为需要调整的共享内存或者文件,length为需要调整的大小。

映射共享内存
创建共享内存后,需要将共享内存映射到调用进程的地址空间,可通过mmap()函数来完成。mmap()函数原型如下:

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

函数成功返回映射后指向共享内存的虚拟地址,失败返回MAP_FAILED值。

参数如下:

  • addr:指向映射存储区的起始地址,通常将其设置为NULL,这表示由系统选择该映射区的起始地址;
  • len:映射的字节数;
  • port:对映射存储区的保护要求,对指定映射存储区的保护要求不能超过文件open模式访问权限。它可以为以下值的或值:
      1. PROT_READ:映射区可读);
      2. PROT_WRITE:映射区可写;
      3. PROT_EXEC:映射区可执行;
      4. PROT_NONE:映射区不可访问。
  • flag:映射标志位,可为以下值的或值:
      1. MAP_FIXED:返回值必须等于addr。因为这不利于可移植性,所以不鼓励使用此标志;
      2. MAP_SHARED:多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化;
      3. MAP_PRIVATE:多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化。
  • fd:要被映射的文件描述符或者共享内存描述符;
  • offset:要映射字节在文件中的起始偏移量。

取消共享内存映射
已经建立的共享内存映射,可通过munmap()函数用来取消。munmap()函数原型如下:

#include <sys/mman.h>
int munmap(void *addr, size_t length);

函数成功返回0,否则返回-1;
参数addr为mmap()函数返回的地址,length是映射的字节数。取消映射后再对映射地址访问会导致调用进程收到SIGSEGV信号。


3. 信号量


3.1 信号量概述


多进程编程中需要关注进程间同步及互斥。同步是指多个进程为了完成同一个任务相互协作运行,而互斥是指不同的进程为了争夺有限的系统资源(硬件或软件资源)而相互竞争运行。

信号量是用来解决进程间的同步与互斥问题的一种进程间通信机制,它是一个特殊的变量,变量的值代表着关联资源的可用数量。若等于0则意味着目前没有可用的资源。

根据信号量的值可以将信号量分为二值信号量和计数信号量:

  • 二值信号量:信号量的值只有0和1值,若资源被锁住,信号量值为0,若资源可用则信号量值为1;
  • 计数信号量:信号量的值在0到一个大于1的值(最大32767)。该计数表示可用的资源的个数。

信号量只能进行的两个原子操作:P操作:V操作。

P原子操作和V原子操作的具体定义如下:

  • P操作:如果有可用的资源(信号量值>0),则占用一个资源(给信号量值减1);如果没有可用的资源(信号量值=0),则进程被阻塞直到系统将资源分配给该进程(进入信号量的等待队列,等到资源后唤醒该进程)。
  • V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(给信号量值加1)

POSIX提供两类信号量:有名信号量基于内存的信号量(也称无名信号量)。有名信号量可以让不同的进程通过信号量的名字获取到信号量,而内存的信号量只能放置在进程间共享内存区域中。有名信号量与无名信号量的初始化和销毁方式与内存的信号量不同,操作流程区别如下图所示:
在这里插入图片描述


编译POSIX信号量程序需要加上-pthread参数。

3.2 信号量的操作


创建或打开有名信号量
使用有名信号量前需要先创建或打开信号量,可使用sem_open()函数来完成。sem_open()函数的原型如下:

#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);

信号量的类型为sem_t,该结构里记录着当前共享资源的数目。sem_open()函数成功返回指向信号量的指针,失败返回SEM_FAILED。参数如下:
参数name为信号量的名字,两个不同的进程可以通过传递相同的名字打开同一个信号量;

  • oflag可以是为以下值的或值:
      1. O_CREAT:如果name指定的信号量不存在则创建,此时必须给出mode和value值;
      2. O_EXCL:如果name指定的信号量存在,而oflag指定为O_CREAT | O_EXCL,则- sem_open()函数返回错误。
  • mode为信号量的权限位,类似open()函数;
  • value为信号量的初始化值。

关闭有名信号量
当不再需要使用有名信号量是可以用sem_close()函数用来关闭,它的原型如下:

#include <semaphore.h>
int sem_close(sem_t *sem);

函数成功返回0,失败返回-1,参数sem为需要关闭的信号量的指针。


初始化基于内存信号量
使用基于内存的信号量之前需要先用sem_init()函数完成初始化,它的原型如下:

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

函数成功返回0,失败返回-1;
参数sem为需要初始化信号量的指针;pshared值如果为0表示该信号量只能在线程内部使用,否则为进程间使用。在进程间使用时,该信号量需要放在共享内存处;value为信号量的初始化值,代表的资源数。


P操作
信号量的P操作由sem_wait()函数来完成,它的函数原型如下:

#include <semaphore.h>
int sem_wait(sem_t *sem);

如果信号量的值大于0,sem_wait()函数将信号量值减1并立即返回,代表着获取到资源,如果信号量值等于0则调用进程(线程)将进入睡眠状态,直到该值变为大于0时才将信号量减1才返回。

函数成功返回0,否则返回-1;参数sem为需要操作的信号量。


V操作
信号量的V操作有sem_post()函数来完成,它的函数原型如下:

#include <semaphore.h>
int sem_post(sem_t *sem);

当一个进程(线程)使用完某个信号灯时,它应该调用sem_post来告诉系统申请的资源已经用完。sem_post()函数与sem_wait函数的功能正好相反,它把所指定的信号灯的值加1,然后唤醒正在等待该信号灯值变为正数的任意进程(线程)。

函数成功返回0,否则返回-1;参数sem为需要操作的信号量。


销毁基于内存的信号量
销毁基于内存的信号量可使用sem_destroy()函数来完成,它的原型如下:

#include <semaphore.h>
int sem_destroy(sem_t *sem);

该函数只能销毁由sem_init()初始化的信号量。销毁后该信号量将不能再被使用。

函数成功返回0,否则返回-1。参数sem指出需要销毁的信号量。


删除有名信号量
当相关的进程都已完成对有名信号量的使用时,可以用sem_unlink()函数用来删除它,以释放资源。sem_unlink()函数原型如下:

#include <semaphore.h>
int sem_unlink(const char *name);

该函数成功返回0,失败返回-1。


4. mmap和shm共享内存的区别


参考文章:

  1. mmap和shm共享内存的区别和联系
  2. 共享内存 mmap shmget 区别

5. 实战应用


【1】【Linux开发实践】使用共享内存遇到的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

智驾

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

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

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

打赏作者

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

抵扣说明:

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

余额充值