进程间通信方法(inter-procession communication)

在fork之后,子进程和父进程就分叉了,至于父子进程哪一个先运行,是由操作系统进程调度算法决定的。如果想要父子协同工作,可以采用原语的办法解决。
那么当多个进程需协同工作共同处理某一个任务时,这时就需要进程间的同步和数据交流。常用的进程间通信方法有:
1.信号(signal):信号用于通知接收进程某个事件已经发生。
2.管道(pipe):管道是一种半双工的通信方式,数据在同一时刻只能单向流动,而且只能在具有亲缘关系的进程(通常是父子进程)间使用。
3.命名管道(named pipe):也是半双工的通信方式,但是可用于不具备亲缘关系的进程之间通信。
4.命名socket或unix域socket(named socket 或 unix domain socket):socket也是一种进程间通信方式,但是,与其他通信机制不同的是,它可用于不同进程间的进程的通信。
5.信号量(semaphore):信号量是一个计数器,可用来控制多个进程对共享资源的访问。常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也来访问该共享资源,因此,信号量常作为不同进程间以及同一进程中不同线程间的同步手段。

一、信号(signal)

信号是linux系统中用于进程间通信的一种方式,一个信号可以在任一时刻发送给某一个进程,而不必知道该进程的状态,如果接收进程并未处于执行状态,那么该信号就会由linux内核保存起来,当该接收进程被执行时,再将该信号发送给该接收进程;如果该接收进程把该信号设置为阻塞,则该信号的传递将被延迟,直到阻塞取消才会把该进程传递给进程。
linux提供了几十种不同的信号,分别表示不同的含义,不同的信号之间由它们的值进行区分,但是在程序中,通常由信号的名字来表示一个信号。信号是在软件层次上对中断的一种模拟,是一种异步通信方式,内核和用户空间可以通过信号来进行交互。
我们知道,父进程创建子进程后,父子进程运行先后是由操作系统进程调度策略来决定的,如果我们想要确保父子的先后运行顺序,就可以采用信号的方法来解决。
下面的程序确保了当父进程先运行时,父进程就进入循环休眠状态,直到子进程给父进程发送一个信号后,父进程就跳出循环继续执行,这样就可以确保子进程先完成其自己的任务。同样子进程在执行完任务后,就等待父进程给它发送一个信号后才退出,而父进程则通过调用wait系统调用后再退出。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int g_child_stop = 0;
int g_parent_run = 0;
void sig_child(int signum)
{
 if( SIGUSR1 == signum )
 {
 g_child_stop = 1;
 }
}
void sig_parent(int signum)
{
 if( SIGUSR2 == signum )
 {
 g_parent_run = 1;
 }
}
int main(int argc, char **argv)
{
 int pid;
 int wstatus;
 signal(SIGUSR1, sig_child);
 signal(SIGUSR2, sig_parent);
 if( (pid=fork()) < 0 )
 {
 printf("Create child process failure: %s\n", strerror(errno));
 return -2;
 }
 else if(pid == 0)
 {
 /* child process can do something first here, then tell parent process to start running */
 printf("Child process start running and send parent a signal\n");
 kill(getppid(), SIGUSR2);
 while( !g_child_stop )
 {
 sleep(1);
 }
 
 printf("Child process receive signal from parent and exit now\n");
 return 0;
 }
 printf("Parent hangs up untill receive signal from child!\n");
 while( !g_parent_run )
 {
 sleep(1);
 }
 /* parent process can do something here, then tell child process to exit */
 printf("Parent start running now and send child a signal to exit\n");
 kill(pid, SIGUSR1);
 /* parent wait child process exit */
 wait(&wstatus);
 printf("Parent wait child process die and exit now\n");
 return 0;
}
ipc $ ./a.out 
Parent hangs up untill receive signal from child!
Child process start running and send parent a signal
Parent start running now and send child a signal to exit
Child process receive signal from parent and exit now
Parent wait child process die and exit now

二、管道(pipe)

管道的实质就是一个内核缓冲区。进程以先进先出的方式从缓冲区读取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程顺序的从缓冲区中读取数据。可以把该缓冲区看成一个循环队列,读和写的位置都是自动增加的。当缓冲区读空或者写满时,有一定的规则控制读进程或者写进程是否进入等待队列,当缓冲区可读或可写时,就唤醒等待队列中的进程进行读或写。
通过调用pipe函数来创建一个管道:

#include<unistd.h>
int  pipe(int 	fd[2])			//成功返回0,出错返回-1

fd[0]是用作读的文件描述符,fd[1]是用作写的文件描述符。fd[1]的输出是fd[0]的输入。
一般我们pipe出一个管道,然后在fork一个子进程,子进程会继承父进程管道两端的文件描述符,然后根据是子进程往父进程写还是父进程往子进程写来关闭相应的文件描述符,实现父子进程的通信。

三、命名管道(fifo)

未命名的管道只能在具有亲缘关系的进程间交换数据,而命名管道可以在两个不具有亲缘关系的进程间交换数据。命名管道与管道的不同之处在于命名管道提供一个路径与之关联,以fifo的文件形式存在于系统中,命名管道在磁盘上有对应的节点,但没有数据块,即只是拥有一个名字和相应的访问权限,通过mknode或mkfifo函数来建立的。当建立了之后,任何拥有访问权的进程都可以通过文件名将其打开和对其进行读写,当不再被进程使用时,fifo在内存中释放,但磁盘节点仍然存在。

四、命名socket

socket除了可以用来在网络中不同主机之间的通信以外,也可以用来在同一主机的不同进程之间通信,且建立的通信是双向的通信,这种用来在同一主机的不同进程间的socket就叫命名socket(Named socket)或Unix域socket(Unix domain socket)。Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务通信的一种方式,也是进程间通信的一种方式。
命名socket与普通的TCP/IP网络 socket相比具有以下特点:
1.UNIX域套接字域传统套接字的区别是用路径名表示协议族的描述,ls -l看到的该文件类型为 s
2. UNIX域套接字域TCP套接字相比,在同一台主机的传输速度前者是后者的两倍。UNIX域套接字仅仅复制数据,并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不产生顺序号,也不需要发送确认报文。
3. UNIX域套接字可以在同一台主机上各进程之间传递文件描述符。

命名socket与TCP/IP网络socket通信使用的是同一套接口,只是地址结构与某些参数不同:TCP/IP网络socket通过IP地址和端口号来标识,而UNIX域协议中使用普通文件系统路径名标识。所以命名socket和普通socket只是在创建socket和绑定服务器标识的时候与网络socket有区别,具体如下:

int socket(int domain, int type, int protocol);
说明:创建一个socket,可以是TCP/IP网络socket,也是命名socket,具体由domain参数决定。该函数的返回值为生成的套接字描述
符。
参数domain指定协议族:对于命名socket/Unix域socket,其值须被置为 AF_UNIX或AF_LOCAL;如果是网络socket则其值应该
为 AF_INET;
参数type指定套接字类型,它可以被设置为 SOCK_STREAM(流式套接字)或 SOCK_DGRAM(数据报式套接字);
参数protocol应被设置为 0

SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用struct sockaddr_un 类型的变量。

struct sockaddr_un {
 sa_family_t sun_family; /* AF_UNIX */
 char sun_path[UNIX_PATH_MAX]; /* 路径名 */
};

这里面有一个很关键的东西,命名socket命名方式有两种。一是普通的命名,socket会根据此命名创建一个同名的socket文件,客户端连接的时候通过读取该socket文件连接到socket服务端。这种方式的弊端是服务端必须对socket文件的路径具备写权
限,客户端必须知道socket文件路径,且必须对该路径有读权限。另外一种命名方式是抽象命名空间,这种方式不需要创建socket文件,只需要命名一个全局名字,即可让客户端根据此名字进行连接。后者的实现过程与前者的差别是,后者在对地址结
构成员sun_path数组赋值的时候,必须把第一个字节置0,即sun_path[0] = 0。

五、信号量

在创建了子进程之后,是父进程先运行还是子进程先运行由操作系统进程调度策略决定。如果我们想让子进程先运行,就可以在父进程中加入sleep函数,但sleep函数并只能保证在子进程先运行,但并不能保证在子进程执行完后再执行父进程,如果想让子进程完全执行完后再执行父进程,就可以使用信号量(Semaphore)来解决它们之间的同步问题。信号和信号量是不同的,虽然它们都能实现进程间的同步和互斥,但信号是通过信号处理器来实现的,而信号量是通过P,V操作来实现的。
在多任务操作系统环境下,多个进程会同时运行,并且一些进程间可能会存在一定的关联。在多个进程协同完成一个任务时,就形成了进程间的同步关系。不同的进程间,为争夺有限的系统资源(硬件或软件资源),就形成的进程间的互斥关系。
进程间存在同步和互斥的根源在于临界资源,临界资源是同一时刻只允许有限个进程(通常是一个)访问的资源,通常包括硬件资源(处理器、内存、存储器及其它外围设备等)和软件资源(共享代码段、共享结构和变量等)。访问临界资源的代码就叫做临界区,临界区本身也是一种临界资源。信号量是用来解决进程间同步和互斥的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量的两种原子操作(P/V)。其中,信号量对应于一种资源,是一个非零的整数值,信号量值对应于当前可用的该资源的数量,如果信号量值为0,表示当前没有可用的这类资源。
PV原子操作的具体定义如下:
P操作:如果有可用的资源(信号量值>0),则此操作所在的进程占用一个资源(此时信号量值减1,进入临界区代码);
如果没有可用的资源(信号量值=0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(即信号量值加1)。

在Linux系统中,使用信号量通常分为以下4个步骤:

  1. 创建信号量或获得在系统中已存在的信号量,此时需要调用 semget() 函数。不同进程通过使用同一个信号量键值来获得同一个信号量。
  2. 初始化信号量,此时使用 semctl() 函数的SETVAL操作。当使用互斥信号量时,通常将信号量初始化为1。
  3. 进行信号量的PV操作,此时,调用 semop()函数。这一步是实现进程间的同步和互斥的核心工作部分。
  4. 如果不需要信号量,则从系统中删除它,此时使用semctl()函数的 IPC_RMID操作。需要注意的是,在程序中不应该出现对已经被删除的信号量的操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值