进程间通信(IPC)
进程间通信的原因:数据传输、资源共享、通知事件、进程控制
进程间通信方式:
1. 管道(pipe)和命名管道(FIFO)(最古老的IPC,但目前很少使用)
2. 信号(signal)
3. 消息队列(重点)
4. 共享内存
5. 信号量
6. 套接字(socket)
A. 管道通信:单向的、先进先出的
无名管道用于父进程和子进程件的通信,命名管道用于运行于同一系统中的任意两个进程间的通信。
无名管道由pipe() 函数创建:
int pipe ( int filedes [2]);
当一个管道建立时,它会创建两个文件描述符:
filedes[0] 用于读管道, filedes[1]用于写管道。
父进程fork一个子进程,子进程会继承父进程创建的管道。必须在fork()前调用pipe(),否则子进程不会继承文件描述符。
命名管道由mkfifo()函数创建:
int mkfifo(const char *pathname ,mode_t mode )
pathname: FIFO文件名
mode:属性
一旦创建了一个FIFO,就可以用open打开它,一般的文件访问函数(close,read,write等)都可用于FIFO。
**管道文件不存储数据,只是倒一下数据。**
B. 信号通信
信号处理:
1. 忽略此信号:大多数信号都按这种方式处理,有两种信号SIGKILL和SIGSTOP不能被忽略,因为它们向超级用户提供了一种终止或停止进程的方法。
2. 执行用户希望的动作:通知内核在某种信号发生时,调用一个用户函数。
3. 执行系统默认动作:对大多数信号的系统默认动作是终止该进程。
发送信号的主要函数有kill和raise。
区别:kill既可以向自身发送信号,也可以向其它进程发送信号。raise函数只能向进程自身发送信号。
int kill (pid_t pid, intsigno)
int raise(int signo )
使用alarm函数可以设置一个时间值,产生SIGALRM信号
unsigned int alarm (unsigedint seconds)
pause函数使调用者进程挂起直至捕捉到一个信号。
int pause (void);
只有执行了一个信号处理函数后,挂起才结束。
信号处理的方式主要有两种,一种是使用简单的signal函数,另一种是使用信号集。
typedef void(*sighandler_t)(int );
sighandler_t signal (intsignum, sighandler_t handler);
**共享内存、消息队列和信号量集都是XSI IPC,遵循相同的规范,因此编程有很多共性的地方。XSI IPC的共性:
1. 创建/获取 IPC结构,必须先提供一个外部的key。
2. 每个IPC结构都有一个唯一的ID与之对应,用key可以拿到id。
3. 外部key的类型是key_t,获得key的方式有三种:
a) 使用宏 IPC_PRIVATE 做key,这个key基本不用,因为这个key只能创建,不能获取。(有但不使用)
b) 可以用函数ftok()创建key。
c) 可以在一个公共的头文件中定义每个使用的key,key本身就是一个整数。
4. 函数 xxxget() 可以用key创建/获得 ID,比如:
shmget() / msgget()
5. 每种IPC结构都提供了一个 xxxctl()函数,可以修改、删除、查询IPC结构。
6. 使用key新建IPC结构时,参数flg一般都是IPC_CREAT|权限。
7. xxxctl()函数中,cmd支持以下宏:
IPC_STAT : 用于查询
IPC_SET : 用于修改
IPC_RMID : 用于删除
8. XSI IPC 为每个IPC结构设置了一个ipc_perm 权限结构,通过xxxctl 函数 ,
可以修改uid,gid,mode字段,结构其它成员不能修改。
IPC结构可以使用命令 查看或者删除:
ipcs - 可以查询IPC
ipcrm - 可以删除IPC
ipcs-a 查看所有IPC
-m查看共享内存
-q 查看消息队列
-s 查看信号量集
ipcrm 删除时,需要提供 IPC的ID。
C. 共享内存
多个进程共享的一部分物理内存,是进程间共享数据的一种最快方法,一个进程向共享内存区写入数据,共享这个内存区域的其他所有进程就可以立刻看到其中的内容。映射物理内存叫挂接,用完以后解除映射叫脱接。
共享内存实现的步骤:
1. 先获得key,可以使用头文件或者 ftok()
2. 用key获得/创建一个共享内存的ID,函数shmget()。
3. 映射共享内存,挂接,函数shmat()。
4. 数据交互。IPC
5. 解除映射,脱接,函数shmdt()。
6. 如果确定共享内部不再使用,可以使用shmctl()函数删除。
ftok() 通过一个真实存在的路径 + 项目ID(0-255)生成一个key。
key_tftok( const char * path, int id) 项目名(不为0即可)
**使用同一项目ID,对于不同文件的两个路径可能产生相同的键。**
int shmget(key_t key,size_t size,int flag)
flag新建时给IPC_CREAT|权限,获取时 0 。返回共享内存的ID,失败返回-1。
**共享内存的缺点是多个进程同时写的时候,数据完全混乱了。**
D. 消息队列(重点)
消息队列设计更加的合理,先把数据封入消息中,把消息存入队列。进程可以按照一定的规则添加新消息;另一些进程可以从消息队列中读走消息。消息队列也是采用内存做 交互媒介,系统内核管理一个队列,队列中存放着数据。
目前主要有两种类型的消息队列:
POSIX消息队列和系统V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有内核重启或者人工删除时,该消息队列才会被删除。
消息队列的使用步骤:
1. 用ftok()获得外部的key。
2. 用msgget() 创建/获取 消息队列的ID。
3. 放入数据(msgsnd()) 或者 取出数据(msgrcv())。
4. 如果确定不再使用消息队列,用msgctl()删除消息队列。
IPC_CREAT 创建新的消息队列;IPC_EXCL与IPC_CREAT一同使用表示如果要创建的消息队列已经存在,则返回错误。IPC_NOWAIT读写消息队列无法满足时,不阻塞。
消息队列的正规用法:
消息分为 有类型消息和无类型消息,无类型消息编程比较简单,但接收数据时无法细分,只能先入先出。有类型消息编程比较规范,接收数据时可以区分,但不是先入先出。
消息是一个结构,格式如下:
struct 结构名{ //结构名程序员可以自定义
longmtype; //消息类型, 消息类型>0,第一个成员必须是消息类型
char mtext[1];//消息数据的首地址,数据区,支持任意类型的数据
};
int msgsnd(int msgid,void* addr,size_tsize,int flag)
参数:msgid 就是消息队列的ID,用key可以获得。
addr是消息的首地址,也就是消息类型的首地址
size 是消息中 数据区的大小,不算类型。(算类型也可以)
flag 可以为 0 代表阻塞, IPC_NOWAIT 非阻塞(满了直接返回-1)
ssize_t msgrcv(int msgid,void* addr,size_tsize,int msgtype, int flag)
参数:msgid和addr与msgsnd一样,size是接收buffer的大小,
flag和msgsnd 一样
msgtype 决定了接收 何种类型的消息(消息类型必须大于0)
> 0 接收特定类型的消息
0 接受 任意类型的消息
< 0 接收类型小于等于msgtype绝对值的消息,从小到大
成功返回实际接收到的字节数,失败返回 -1 。
函数msgctl(msgid,IPC_RMID,0) 可以删除消息队列。
**消息队列的删除和共享内存的删除机制不同,共享内存的删除只是做了个删除标记,不确保马上删除,只有挂接数为0的才能被删除。消息队列随时可以删除,即使队列中还有消息依然会被删除。**
E. 信号量
与其它进程间通信不大相同,主要用途是保护临界资源。进程可以根据它判断是否能够访问某些共享资源。处了用于访问控制外,还可以用于进程同步。
信号量分类:
二值信号量:信号量值只能取0或1,类似于互斥锁。但两者有不同:信号量强调共享资源,只要共享资源可用,其他进程同样可以修改信号量的值;互斥锁更强调进程。
计数信号量:信号量的值可以取任意非负值。
创建和打开:
int semget ( key_tkey, int nsems , int semflg );
key: 键值,由ftok获得;nsems: 指定打开或者创建的信号量集中信号量的数目;
int semop (int semid , struct sembuf*sops, unsigned nspos );
功能:对信号量进行控制。
Semid: 信号量集的ID;sops:s 操作数组,表明要进行什么操作;nsops:元素个数
struct sembuf {
unsigned short sem_num;
short sem_op;
short sem_flg;
}