什么是进程间通信
进程间通信就是在不同进程之间传播或交换信息。
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+轮询方式来解决问题
进程间通信的目的是什么
通信方式有哪些
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 |
函数原型 |
Int pipe(int fd[2]) |
函数传入值 |
fd[2]:管道的两个文件描述符 |
函数返回值 |
如果系统调用成功, 返回0。如果系统调用失败返回-1: |
注意:fd[0]用于读取管道,fd[1]用于写入管道。该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。一般文件的I/O函数都可以用于管道,如close、read、write等等。
2.有名管道
创建有名管道用mkfifo()。
头文件
#include
#include
函数原型
int mkfifo(const char * pathname, mode_t mode)
函数传入值
Pathname:要创建的的管道
Mode:设置管道权限
函数返回值
若成功则为0,若出错返回-1
FIFO相关出错信息:
EACCES(无存取权限)
EEXIST(指定文件不存在)
ENAMETOOLONG(路径名太长)
ENOENT(包含的目录不存在)
ENOSPC(文件系统余空间不足)
ENOTDIR(文件路径无效)
EROFS(指定的文件存在于只读文件系统中)
信号通信
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP。
kill()函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组,它不仅可以中止进程(实际上发出SIGKILL信号),也可以向进程发送其他信号。
kill()函数语法:
头文件 |
#include #include |
函数原型 |
int kill(pid_t pid,int sig) |
函数传入值 |
Pid: 正数:要发送信号的进程号 0:信号被发送到所有的当前进程在同一进程组的进程 -1:信号发给所有的进程表中的进程 <-1:信号发给进程组号为-pid的每一个进程 Sig:信号 |
函数返回值 |
若成功则为0,若出错返回-1。 |
raise()函数:
功 能: 向正在执行的程序发送一个信号,允许进程向自身发送信号。 头文件:#include
返回值:返回零值为成功,非零为失败。
signal()函数:
头文件:#include
原型: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()函数语法
头文件
#include
#include
#include
函数原型
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
#include
#include
函数原型
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。
头文件
#include
#include
#include
函数原型
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
#include
#include
函数原型
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。
头文件
#include
#include
#include
函数原型
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(超出限制)
头文件
#include
#include
#include
函数原型
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
#include
#include
函数原型
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对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
头文件 |
#include #include #include |
函数原型 |
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核心内存不足。 |
头文件 |
#include #include #include |
函数原型 |
Char * shmat(int shmid,const void *shmaddr,int shmflg) |
函数传入值 |
Shmid: 需要映射的共享内存区标识符 Shmaddr:将共享内存映射到指定地址(若为0则表示系统自动分配地址并映射) Shmflg:SHM_RDONLY:只读 默认0:可读写 |
函数返回值 |
成功返回被映射的段地址;不成功返回-1, |
shmdt函数语法:
头文件 |
#include #include #include |
函数原型 |
Int shmdt(const void *shmaddr) |
函数传入值 |
Shmaddr:被映射的共享内存段地址. |
函数返回值 |
成功返回0;不成功返回-1, |
shmctl()函数语法:
头文件 |
#include #include #include |
函数原型 |
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将到后面专门写一篇