您的位置:首页 > 运维架构 > Linux

linux 进程间通信详解

2015-07-19 18:33 489 查看
http://blog.csdn.net/jmq_0000/article/details/7174261



什么是进程间通信

进程间通信就是在不同进程之间传播或交换信息。

linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来。

早期UNIX进程间通信、基于System V进程间通信、基于Socket进程间通信和POSIX进程间通信。UNIX进程间通信方式包括:管道、FIFO、信号。System V进程间通信方式包括:System V消息队列、System V信号灯、System V共享内存、POSIX进程间通信包括:posix消息队列、posix信号灯、posix共享内存。

多进程优点:

每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
通过增加CPU,就可以容易扩充性能;
可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
多进程缺点:

逻辑控制复杂,需要和主程序交互;
需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
多进程调度开销比较大

多线程的优点:

无需跨进程边界;
程序逻辑和控制方式简单;
所有线程可以直接共享内存和变量等;
线程方式消耗的总资源比进程方式好;

多线程缺点:

每个线程与主程序共用地址空间,受限于2GB地址空间;
线程之间的同步和加锁控制比较麻烦;
一个线程的崩溃可能影响到整个程序的稳定性;
到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU

最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题

进程间通信的目的是什么

数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
  共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
  通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
  进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

通信方式有哪些

linux下进程间通信的几种主要方式:

(1)管道(pipe)和有名管道(FIFO)

(2)信号(signal)

(3)消息队列

(4)共享内存

(5)信号量

(6)套接字(socket)

什么是管道

管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。通常有种限制,一是半双工,只能单向传输;二是只能在父子进程间使用。

有名管道(也叫FIFO,因为管道工作在先入先出的原则下,第一个写入管道的数据也是第一个被读出的数据)。与管道不同,FIFO不是临时的对象,它们是文件系统中真正的实体,可以用mkfifo命令创建。只要有合适的访问权限,进程就可以使用FIFO。FIFO的打开方式和管道稍微不同。一个管道(它的两个file数据结构、VFS I节点和共享数据页)是一次性创建的,而FIFO已经存在,可以由它的用户打开和关闭。Linux必须处理在写进程打开FIFO之前读进程对它的打开,也必须处理在写进程写数据之前读进程对管道的读。除此以外,FIFO几乎和管道的处理完全一样,而且它们使用一样的数据结构和操作。

管道的编程

1.无名管道

创建一个简单的管道,可以使用系统调用pipe()。它接受一个参数,也就是一个包括两个整数的数组。如果系统调用成功,此数组将包括管道使用的两个文件描述符。创建一个管道之后,一般情况下进程将产生一个新的进程。

系统调用:pipe();
头文件

#include<unistd.h>

函数原型

Int pipe(int fd[2])

函数传入值

fd[2]:管道的两个文件描述符

函数返回值

如果系统调用成功,

返回0。如果系统调用失败返回-1:

errno=EMFILE(没有空亲的文件描述符)

EMFILE(系统文件表已满)

EFAULT(fd数组无效)

注意:fd[0]用于读取管道,fd[1]用于写入管道。该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。一般文件的I/O函数都可以用于管道,如close、read、write等等。

2.有名管道

创建有名管道用mkfifo()。

头文件

#include <sys/stat.h>

#include <sys/types.h>

函数原型

int mkfifo(const char * pathname, mode_t mode)

函数传入值

Pathname:要创建的的管道

Mode:设置管道权限

函数返回值

若成功则为0,若出错返回-1

FIFO相关出错信息:

EACCES(无存取权限)

EEXIST(指定文件不存在)

ENAMETOOLONG(路径名太长)

ENOENT(包含的目录不存在)

ENOSPC(文件系统余空间不足)

ENOTDIR(文件路径无效)

EROFS(指定的文件存在于只读文件系统中)

有名管道(FIFO)读写
对于读进程
若该管道是阻塞打开,且当前FIFO内没有数据,则对读进程而言将一直阻塞到有数据写入。
若该管道是非阻塞打开,则不论FIFO内是否有数据,读进程都会立即执行读操作。即如果FIFO内没有数据,则读函数将立刻返回0。
对于写进程
若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入。
若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败

信号通信
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。
如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程 。
1.进程相应信号的方式

忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP。

捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。
执行缺省操作,Linux对每种信号都规定了默认操作

信号发送和捕捉
信号发送:kill()和raise()

kill()函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组,它不仅可以中止进程(实际上发出SIGKILL信号),也可以向进程发送其他信号。

kill()函数语法:

头文件

#include <signal.h>

#include <sys/types.h>

函数原型

int kill(pid_t pid,int sig)

函数传入值

Pid:

正数:要发送信号的进程号

0:信号被发送到所有的当前进程在同一进程组的进程

-1:信号发给所有的进程表中的进程

<-1:信号发给进程组号为-pid的每一个进程

Sig:信号

函数返回值

若成功则为0,若出错返回-1。

raise()函数:
功 能: 向正在执行的程序发送一个信号,允许进程向自身发送信号。  
头文件:#include <signal.h>
用 法:int
raise(int signo);
返回值:返回零值为成功,非零为失败。

信号捕捉:alarm()、pause()
alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号 。
函数原型:unsigned
int alarm(unsigned
int seconds);//每个进程只能有一个闹钟时钟。

pause()函数是用于将调用进程挂起直至捕捉到信号为止。这个函数很常用,通常可以用于判断信号是否已到。
函数原型:int pause(void);//只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回-1,并将errno设置为EINTR。

信号的处理:signal、sigaction
signal()处理信号时,只需要指出要处理的信号和处理函数即可。它主要是用于前32种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解,因此也受到很多程序员的欢迎。

signal()函数:

头文件:#include <signal.h>

原型:void
(*signal(int
signo,
void (*func)(int)))(int);

参数:signo要处理的信号;

func:SIG_IGN忽略信号

SIG_DFL默认处理方式

自定义的信号处理函数指针

sigaction()函数:

sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)的第二个参数是一个指向sigaction结构的指针(结构体名称与函数名一样,千万别弄混淆了)。在结构sigaction的实例中,
指定了对特定信号的处理,信号所传递的信息,信号处理函数执行过程中应屏蔽掉哪些函数等。当然,此指针也可以为NULL,进程会以默认方式处理信号。

消息队列

消息队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

消息队列的实现包括创建或打开消息队列、添加消息、读取消息和控制消息队列这四种操作
创建或打开消息队列使用的函数是msgget,这里创建的消息队列的数量会受到系统消息队列数量的限制
添加消息使用的函数是msgsnd函数,它把消息添加到已打开的消息队列末尾
读取消息使用的函数是msgrcv,它把消息从消息队列中取走,与FIFO不同的是,这里可以指定取走某一条消息
控制消息队列使用的函数是msgctl,它可以完成多项功能。

msgget()函数语法

头文件

#include <sys/ipc.h>

#include <sys/msg.h>

#include <sys/types.h>

函数原型

int msgget(key_t key, int msgflg)

函数传入值

Key:ipc键值由ftok()函数建立

Msgflg:权限标识位

函数返回值

若成功则返回消息队列ID,若出错返回-1。

EACCES:指定的消息队列已存在,但调用进程没有权限访问它,而且不拥有CAP_IPC_OWNER权能

EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志

ENOENT:key指定的消息队列不存在同时msgflg中不指定IPC_CREAT标志

ENOMEM:需要建立消息队列,但内存不足

ENOSPC:需要建立消息队列,但已达到系统的限制

msgsnd()函数语法

头文件
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
函数原型
int msgsnd(int msqid,const void *msgp,size_t msgsz, int msgflg)
函数传入值
Msgqid:消息队列的队列ID
Msgp:指向消息结构的指针。该结构Msgbuf通常是
Struct msgbuf
{
Long mtype; /*消息类型*/
Char mtext[1]; /*消息正文*/
}
Msgsz:消息正文的字节数。
Msgflg:IPC_NOWAIT非阻塞方式 、0 阻塞直到发送为止。
函数返回值
若成功则返回0,若出错返回-1。
msgrcv()函数语法:

头文件
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
函数原型
int msgrcv(int msqid, void *msgp,size_t msgsz,long int msgtyp, int msgflg)
函数传入值
Msgqid:消息队列的队列ID
Msgp:消息缓冲区
Msgtyp:0 :接收消息队列中第一个消息
>0:接收消息队列中第一个类型为msgtyp的消息
<0:接收消息队列中不小于msgtyp绝对值的最小的消息
Msgflg:MSG_NOERROR若返回的消息比msgsz多,消息就会截断到msgsz
IPC_NOWAIT / 0同上一个函数
函数返回值
成功返回读出消息的实际字节数,否则返回-1。
msgctl函数语法:

头文件
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
函数原型
int msgrcv(int msqid, int cmd,struct msqid_ds *buf)
函数传入值
Msgqid:消息队列的队列ID
cmd:IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;
IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。
IPC_RMID:删除msqid标识的消息队列;Msgflg:MSG_NOERROR若返回的消息比msgsz多,消息就会截断到msgsz
函数返回值
若成功则返回0,若出错返回-1。

信号量

信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。信号量是用来解决进程之间的同步与互斥问题的一种进程之间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(PV操作)。其中信号量对应于某一种资源,取一个非负的整型值。信号量值指的是当前可用的该资源的数量,若它等于0则意味着目前没有可用的资源。

什么是PV操作
P操作:如果有可用的资源(信号量值>0),则占用一个资源(给信号量值减去一,进入临界区代码);如果没有可用的资源 (信号量值等于0),则被阻塞到,直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程。如果没有进程等待它,则释放一个资源(给信号量值加一)。
信号量编程
第一步:创建信号量或获得在系统已存在的信号量,此时需要调用semget()函数。不同进程通过使用同一个信号量键值来获得 同一个信号量。
第二步:初始化信号量,此时使用semctl()函数的SETVAL操作。当使用二维信号量时,通常将信号量初始化为1。
第三步:进行信号量的PV操作,此时调用semop()函数。这一步是实现进程之间的同步和互斥的核心工作部分。
第四步:如果不需要信号量,则从系统中删除它,此时使用semclt()函数的IPC_RMID操作。此时需要注意,在程序中不应该出现对已经被删除的信号量的操作。
semget()函数语法:

头文件

#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>

函数原型

int semget(key_t key, int nsems,int semflg)

函数传入值

Key:信号量键值。用IPC_PRIVATE创建当前进程的私有信号量。
Nsems:需要创建信号量数目。
Semflg:权限位

函数返回值

如果成功,则返回信号量集的IPC标识符。如果失败,则返回-1:

errno=EACCESS(没有权限)

EEXIST(信号量集已经存在,无法创建)

EIDRM(信号量集已经删除)

ENOENT(信号量集不存在,同时没有使用IPC_CREAT)

ENOMEM(没有足够的内存创建新的信号量集)

ENOSPC(超出限制)

semctl()函数语法:

头文件
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
函数原型
int semctl(int semid,int semnum,int cmd,union semun arg)
函数传入值
Semid:信号量标识符。
Semnum:信号量编号(单个信号时为0)。
Cmd:·IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
·IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
·IPC_RMID将信号量集从内存中删除。
·GETALL用于读取信号量集中的所有信号量的值。
·GETNCNT返回正在等待资源的进程数目。
·GETPID返回最后一个执行semop操作的进程的PID。
·GETVAL返回信号量集中的一个单个的信号量的值。
·GETZCNT返回这在等待完全空闲的资源的进程数目。
·SETALL设置信号量集中的所有的信号量的值。
·SETVAL设置信号量集中的一个单独的信号量的值。
函数返回值
如果成功,则为一个正数。
如果失败,则为-1:errno=EACCESS(权限不够)
EFAULT(arg指向的地址无效)
EIDRM(信号量集已经删除)
EINVAL(信号量集不存在,或者semid无效)
EPERM(EUID没有cmd的权利)
ERANGE(信号量值超出范围)
semop()函数语法:

头文件
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
函数原型
int semop(int semid,struct sembuf *sops,size_t nsops)
函数传入值
Semid:信号量标识符。
Sops:指向信号量操作数组
Struct sembuf{
Short sem_num; /*将要处理的信号量的个数。*/
Short sem_op; /*要执行的操作*/
Short sem_flg;/*操作标志*/
}
Nsops:操作数组sops中的操作个数。
函数返回值
如果成功返回0。
如果失败返回-1
errno=E2BIG(nsops大于最大的ops数目)
EACCESS(权限不够)
EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行)
EFAULT(sops指向的地址无效)
EIDRM(信号量集已经删除)
EINTR(当睡眠时接收到其他信号)
EINVAL(信号量集不存在,或者semid无效)
ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构)
ERANGE(信号量值超出范围)

共享内存

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

共享内存实现的步骤:
1.创建共享内存,这里用到的函数是shmget,也就是从内存中获得一段共享内存区域;
2.映射共享内存,也就是把这段创建的共享内存映射到具体的进程空间中去,这里使用的函数是shmat;
3.使用不带缓冲的I/O读写命令对其进行操作
4.撤销映射的操作,其函数为shmdt。

shmget函数语法:
头文件
#include <sys/ipc.h>

#include <sys/shm.h>

#include <sys/types.h>
函数原型
int shmget(key_t key,int size,int shmflg)
函数传入值
Key:键值

Size:共享内存区大小

Shmflg:主要和一些标志有关。其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当。

IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。

IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
函数返回值
成功返回共享内存的标识符;不成功返回-1,

Errno:

EINVAL参数size小于SHMMIN或大于SHMMAX。

EEXIST预建立key所致的共享内存,但已经存在。

EIDRM 参数key所致的共享内存已经删除。

ENOSPC 超过了系统允许建立的共享内存的最大值(SHMALL )。

ENOENT 参数key所指的共享内存不存在,参数shmflg也未设IPC_CREAT位。

EACCES 没有权限。

ENOMEM核心内存不足。
shmat函数语法:

头文件
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
函数原型
Char * shmat(int shmid,const void *shmaddr,int shmflg)
函数传入值
Shmid: 需要映射的共享内存区标识符
Shmaddr:将共享内存映射到指定地址(若为0则表示系统自动分配地址并映射)
Shmflg:SHM_RDONLY:只读
默认0:可读写
函数返回值
成功返回被映射的段地址;不成功返回-1,
shmdt函数语法:

头文件
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
函数原型
Int shmdt(const void *shmaddr)
函数传入值
Shmaddr:被映射的共享内存段地址.
函数返回值
成功返回0;不成功返回-1,
shmctl()函数语法:

头文件
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
函数原型
int shmctl( int shm_id, int cmd, struct shmid_ds *buf );
函数传入值
shm_id:需要操作的共享内存标识符
cmd:IPC_STAT:取shm_id所指向内存共享段的shmid_ds结构,对参数buf指向的结构赋值
IPC_SET:使用buf指向的结构对sh_mid段的相关结构赋值,只对以下几个域有作用,shm_perm.
uid shm_perm.gid以及shm_perm.mode ,注意此命令只有具备以下条件的进程才可以请求:
1.进程的用户ID等于shm_perm.cuid或者等于shm_perm.uid
2.超级用户特权进程
IPC_RMID:删除shm_id所指向的共享内存段,只有当shmid_ds结构的shm_nattch域为零时,才会真正执行删除命令,否则不会删除该段 注意此命令的请求规则与IPC_SET命令相同
SHM_LOCK:锁定共享内存段在内存,此命令只能由超级用户请求
SHM_UNLOCK:对共享内存段解锁,此命令只能由超级用户请求
函数返回值
成功返回0;不成功返回-1,
SOCKET

套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

socket将到后面专门写一篇

http://blog.csdn.net/jmq_0000/article/details/7174261
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: