进程间通信(信号、消息队列、共享内存、信号量)

进程间通信

目录

进程间通信

1 信号

2 消息队列

3 共享内存

4 信号量


1 信号

1.1信号的概念

        信号(signal)是一种软件中断,是UNIX系统中最为古老的进程之间的通信机制。用于在一个或多个进程之间传递异步信号。它提供了一种处理异步事件的方法,也是进程间惟一的异步通信方式。在Linux中,根据POSIX标准扩展以后的信号机制,不仅可以用来通知某种程序发生了什么事件,还可以给进程传递数据。

1.2信号的来源

信号的来源有很多种方式,按照产生条件的不同可以分为硬件方式软件方式两种。

硬件方式

        当用户在终端上按下某键或某种组合键时,将产生信号;硬件异常产生信号。这些事件通常由硬件检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时的正在运行的程序。

软件方式

        用户在终端下调用kill命令向进程发送任务信号;进程调用kill或sigqueue函数发送信号;当检测到某种软件条件已经具备时发出信号,如SIGALARM信号。

1.3系统定义的信号

        每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。编号34以上的是实时信号,只讨论编号34以下的信号,不讨论实时信号。

使用命令kill -l可以查看系统定义的信号列表

1.4信号的产生发送

函数可以向一个进程主动地发出一个信号,我们可以通过两个函数kill和alarm来发送一个信号。

kill函数

        进程可以通过kill函数向包括它本身在内的其他进程发送一个信号,如果程序没有发送这个信号的权限,对kill函数的调用就将失败,而失败的常见原因是目标进程有另一个用户所拥有。超级用户root除外。

kill函数的原型:

#include 
#include 
int kill(pid_t pid, int sig);

参数说明:

pid:进程号

sig:信号

它的作用把信号sig发送给进程号为pid的进程,成功时返回0。
kill调用失败的原因有三种:
1.给定的信号错误(EINVAL)
2.发送权限不够(EPERM)
3.目标进程不存在(ESRCH)

alarm函数

        alarm函数给我们提供了一个闹钟的功能,进程可以调用alarm函数在经过预定时间后发送一个SIGALRM信号。

alarm函数的原型:

#include 

unsigned int alarm(unsigned int seconds);

参数说明:

函数中seconds为设定的秒数,在经过seconds秒后发送一个SIGALRM信号,如果seconds为0,将取消所有一设置的闹钟请求。alarm函数的返回值时以前设置的闹钟时间的余留秒数,如果返回失败则返回-1。

1.5信号的处理

signal函数:

        程序可用使用signal函数来处理指定的信号,主要通过忽略和恢复其默认行为来工作。signal函数的原型如下:

#include   
void (*signal(int sig, void (*func)(int)))(int); 

        这是一个相当复杂的声明,耐心点看可以知道signal是一个带有sig和func两个参数的函数,func是一个类型为void (*)(int)的函数指针。该函数返回一个与func相同类型的指针,指向先前指定信号处理函数的函数指针。准备捕获的信号的参数由sig给出,接收到的指定信号后要调用的函数由参数func给出。其实这个函数的使用是相当简单的,通过下面的例子就可以知道。注意信号处理函数的原型必须为void func(int),或者是下面的特殊值:
    SIG_IGN:忽略信号
    SIG_DFL:恢复信号的默认行为
        写了很多,通过例子讲解一下,当我们第一次按下终止命令(ctrl+c)时,进程并没有被终止,面是输出OUCH! - I got signal 2,因为SIGINT的默认行为被signal函数改变了,当进程接受到信号SIGINT时,它就去调用函数ouch去处理,注意ouch函数把信号SIGINT的处理方式改变成默认的方式,所以当你再按一次ctrl+c时,进程就像之前那样被终止了。

sigaction函数

        前面我们看到了signal函数对信号的处理,但是一般情况下我们可以使用一个更加健壮的信号接口—sigaction函数。它的原型为:

#include   
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);  

        该函数与signal函数一样,用于设置与信号sig关联的动作,而oact如果不是空指针的话,就用它来保存原先对该信号的动作的位置,act则用于设置指定信号的动作。
sigaction结构体定义在signal.h中,但是它至少包括以下成员:
1.void (*) (int) sa_handler;处理函数指针,相当于signal函数的func参数。
2.sigset_t sa_mask; 指定一个,信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中。信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到。
3.int sa_flags;信号处理修改器;
4.sa_mask的值通常是通过使用信号集函数来设置的。
5.sa_flags,通常可以取以下的值:
        SA_NOCLDSTOP              子进程停止时不产生SIGCHLD信号
        SA_RESETHAND              将对此信号的处理方式在信号处理函数的入口处重置为SAG_DFL
        SA_RESTART                    重启可中断的函数而不是给出EINTR错误
        SA_NODEFER                   捕获到信号时不将它添加到信号屏蔽字中

        现在有一个这样的问题,我们使用signal或sigaction函数来指定处理信号的函数,但是如果这个信号处理函数建立之前就接收到要处理的信号的话,进程会有怎样的反应呢?它就不会像我们想像的那样用我们设定的处理函数来处理了。sa_mask就可以解决这样的问题,sa_mask指定了一个信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中,设置信号屏蔽字可以防止信号在它的处理函数还未运行结束时就被接收到的情况,即使用sa_mask字段可以消除这一竞态条件。


2 消息队列

2.1 消息队列的概念
        消息队列是一种以链表式的结构组织的一组数据,存放在内核中,是由各个进程通过消息队列标识符来引用的一种数据传输方式。它也是由内核来维护,是3个IPC对象中最具有数据操作性的数据传输方式,在消息队列中可以随意根据特定的数据类型来检索消息。当然,为了维护链表,需要更多的内存资源,而且在数据读写上比起共享内存也复杂一些,时间开销也更大一些。

2.2 消息队列的创建
linux下使用msgget函数创建/打开一个队列。函数原型如下:

#include 
#include 
#include 
int msgget(key_t key, int msgflg);

参数说明:

key:消息队列关联的键,函数ftok()的返回值或IPC_PRIVATE。

msgflag:消息队列的建立标志和存取权限。

2.3 消息队列的删除
linux下使用msgctl函数删除一个指定队列,函数原型如下:

#include 
#include 
#include 
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数说明:

msqid:消息队列对象的标识符。所述的msqid_ds数据结构定义在在

cmd:对消息队列进行的操作

2.4 消息队列的读写
linux下使用msgsnd、msgrcv函数来对消息队列进行读写,函数原型如下:

#include 
#include 
#include 
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数说明:

消息队列写操作
        msqid:消息队列的ID
        msgp:指向消息的指针,常用结构体msgbuf,内容如下:
                struct msgbuf
                {
                        long mtype;        //消息类型
                        char mtext[N];    //消息正文
                }
        size:正文的字节数
        flag:IPC_NOWAIT:消息没有发送完成函数也会立即返回
                 0:直到发送完成函数才会返回

消息队列读操作
        msqid:消息队列的ID
        msgp:要接受消息的缓冲区
        size:要接受的消息的字节数
        msgtyp:
                0:接收消息队列中第一个消息
                大于0:接收消息队列中第一个类型为msgtyp的消息
                小于0:接收消息队列中类型值不大于msgtyp的绝对值且类型值又最小的消息
        flag:IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
                 0:若无消息函数一直阻塞 


3 共享内存

        共享内存是所有进程空间通信方式中最快的一种,它是存在于内核级别的资源。在文件系统/proc目录下有对其描述的相应文件。

3.1 共享内存的概念
        共享内存的机制所依赖的原理:在系统内核为一个进程分配一个地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存同时分配给不同的进程。对于每一个共享存储段,内核都为其维护一个shmid_ds类型的结构体,shmid_ds结构体的定义如下:

struct shmid_ds
{
    struct ipc_perm shm_perm;                /* operation perms */
    int shm_segsz;                           /* size of segment (bytes) */
    __kernel_time_t shm_atime;               /* last attach time */
    __kernel_time_t shm_dtime;               /* last detach time */
    __kernel_time_t shm_ctime;               /* last change time */
    __kernel_ipc_pid_t shm_cpid;             /* pid of creator */
    __kernel_ipc_pid_t shm_lpid;             /* pid of last operator */
    unsigned short shm_nattch;               /* no. of current attaches */
    unsigned short shm_unused;               /* compatibility */
    void *shm_unused2;                       /* ditto - used by DIPC */
    void *shm_unused3;                       /* unused */
}; 

        结构体shmid_ds会根据不同的系统内核版本而略有不同,并且在不同的系统中会对共享存储段的大小有限制,在应用时要查询相关的手册。

3.2 共享内存的创建
linux下使用shmget函数创建/打开一块共享内存区。shmget函数函数的原型如下:

#include 
#include 
int shmget(key_t key, size_t size, int shmflg);

参数说明:

key:     标识符的规则

size:    共享存储段的字节数
flag:    读写的权限还有IPC_CREAT或IPC_EXCL对应文件的O_CREAT或O_EXCL
返回值:  成功返回共享存储的id,失败返回-1

3.3 共享内存的连接
linux下使用shmat函数对一块共享内存区进行连接。shmat函数函数的原型如下:

#include 
#include 
void *shmat(int shmid, const void *shmaddr, int shmflg)

参数说明:

msqid:共享内存标识符
shmaddr:指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
shmflg:SHM_RDONLY:为只读模式,其他为读写模式

3.4 共享内存的操作
        由于共享内存这一特殊的资源类型,操作上不同于普通文件,需要特有的操作函数。linux下使用共享内存进行多种操作。共享内存管理的函数原型如下:

#include 
#include 
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

msqid:共享内存标识符
cmd:IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
          IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
          IPC_RMID:删除这片共享内存
buf:共享内存管理结构体

       1. fork后子进程继承已连接的共享内存地址。exec后该子进程与已连接的共享内存地址自动脱离(detach)。进程结束后,已连接的共享内存地址会自动脱离(detach)
        2.函数执行后连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
        3.当对共享内存段操作结束时,应调用shmdt函数,断开共享内存连接,函数原型如下:

#include 
#include 
int shmdt(const void *shmaddr);

参数说明:

shmaddr:连接的共享内存的起始地址

3.5 共享内存的使用注意事项
        共享内存相比其他方式,数据在读写过程中会更透明。当成功导入一块共享内存后,它只是相当于一个字符串指针来指向一块内存,在当前用户下可以随意访问。但是这样做的缺点是在数据写入/数据读出的过程中需要附加的结构控制,同时,在多进程同步/互斥上也需要附加的代码来辅助共享内存机制。
        在共享内存段中都是以字符串的默认结束符为一条信息的结尾,每个进程都遵循这个规则,就不会破坏数据的完整性。


4 信号量

4.1 信号量的概念
        信号量本身不具有数据传输的功能,它只是一种外部资源的标识,通过该标识可以判断外部资源是否可用,信号量在此过程中负责数据的互斥、同步等操作。当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值,以判断相应的资源是否可用;
        1.当信号量的值大于0时,表示有资源可以请求;
        2.当等于0时,表示现在无可用资源,所以进程会进入睡眠状态直至有可用的资源时。

        当进程不再使用一个信号量控制的共享资源时,此信号量的值+1,对信号量的增减均为原子操作,这是由于信号量的主要作用是维护资源的互斥/多进程的同步访问,而在信号量的创建/初始化时,不能保证为原子操作。内核对每个信号集都会设置一个shmid_ds结构,同时用一个无名结构来标识一个信号量。定义因里linux环境的不同而不同。shmid_ds结构定义如下:

struct shmid_ds
{
    struct ipc_perm shm_perm;                /* operation perms */
    int shm_segsz;                           /* size of segment (bytes) */
    __kernel_time_t shm_atime;               /* last attach time */
    __kernel_time_t shm_dtime;               /* last detach time */
    __kernel_time_t shm_ctime;               /* last change time */
    __kernel_ipc_pid_t shm_cpid;             /* pid of creator */
    __kernel_ipc_pid_t shm_lpid;             /* pid of last operator */
    unsigned short shm_nattch;               /* no. of current attaches */
    unsigned short shm_unused;               /* compatibility */
    void *shm_unused2;                       /* ditto - used by DIPC */
    void *shm_unused3;                       /* unused */
}; 

        shmid_ds数据结构表示每个新建的共享内存。当shmget()创建了一块新的共享内存后,返回一个可以用于引用该共享内存的shmid_ds数据结构的标识符。

4.2 信号量的创建
        同共享内存一样,系统中同样需要为信号量定制一系列专有的操作函数(semget、semctl等)。linux下使用函数semget创建/获得一个信号量集ID,原型如下:

#include 
#include 
#include 
int semget(key_t key, int nsems, int semflg);

参数说明:

nsems:信号量集里面的信号量的个数

flag:是信号量的操作类型以及操纵权限

4.3 信号量的操作
        3个IPC对象类型中,信号量集的操作函数相对于其他两个类型的操作函数而言要复杂的多,同样信号量的使用也比其他两个更加广泛。信号量也有自己的专属操作。linux下使用semctl函数来操作,函数原型如下:

#include 
#include 
#include 
int semctl(int semid, int semnum, int cmd, ...);

semid:信号量集IPC标识符

semnum:操作信号在信号集中的编号,第一个信号的编号是0。

cmd:对信号进行的操作

你可能感兴趣的:(linux)