多进程与多线程
使用有名管道
实现双向通信
时,由于读管道是阻塞读的,为了不让“读操作”阻塞“写操作”,使用了父子进程来多线操作,
1)父进程这条线:读管道1
2)子进程这条线:写管道2
实际上:凡是涉及到多线操作的,基本都使用多线程来实现
1)主线程:读管道1 2)次线程:写管道2
-
线程和进程都是
并发运行
的,但是线程和进程各自的使用的场合有所不同多线使用
多线程
更省计算机cpu和内存的开销创建出并发运行的线程目的-----------
多线操作
程序必须要去运行一个新程序时,此时必须涉及到
多进程
- 这里并发运行的主要目的并不是为了多线操作,而是为了单独的去执行新程序
执行新程序时,我们只能使用多进程来操作,因为线程是不可能去执行一个新程序的
System V IPC
无名管道和有名管道都是UNIX系统早期提供的比较原始的一种进程间通信方式
后来Unix系统升级到第5版本时,又提供了三种新的IPC通信方式
- 消息队列
- 信号量
- 共享内存
System V就是
系统第5版本
的意思后来的Linux也继承了unix的这三个通信方式
管道的机制
- 管道的本质就是一段缓存
- Linux OS内核是以文件的形式来管理管道
我们都是使用
文件描述符
以文件
的形式来操作:无名管道和有名管道
操作管道时:
除了pipe和mkfifo这两个函数外,其它的像read、write、open都是文件io函数
System V IPC
System V IPC与管道有所不同:
它完全使用了不同的实现机制,与文件没任何的关系
也就是说
内核
不再以文件
的形式来管理System V IPC
- 对于System V IPC,OS内核提供了全新的API
- System V IPC时,不存在亲缘进程一说,任何进程之间都可以使用System V IPC来通信
System V IPC标识符
- 这个“标识符”就是文件描述符的替代者,但是它是专门给System V IPC使用的
- 不能使用文件IO函数来操作“标识符”,只能使用System V IPC的特有API才能操作
如何得到ipc标识符
调用某API创建好某个“通信结构”以后,API就会返回一个唯一的“标识符”
比如创建好了一个“消息队列”后,创建的API就会返回一个唯一标识消息队列的“标识符”
ipc标识符作用
如果创建的是消息队列的话:
-
进程通过消息队列唯一的标识符,就能找到创建好的“消息队列”
-
使用这个消息队列,进程就能读写数据,从而实现进程间通信
可以读写数据就是实现了通信
也就是标识符就是可是识别传建好的system V IPC
消息队列
本质
消息队列的本质:由内核创建的用于存放消息的链表
由于是存放消息的,所以把这个
链表
称为了消息队列
如何存放消息
消息队列这个链表有很多的节点
,链表上的每一个节点就是一个消息
- 注意是一个双向链表
- 每个消息由两部分组成
- 1)
消息编号
:识别消息用 - 2)
消息正文
:真正的信息内容
- 1)
发送接收消息过程
1.发送消息
(a)进程先封装一个消息包
(b)调用相应的API发送消息
这个消息包其实就是如下类型的一个结构体变量:
----封包时将消息编号和消息正文写到结构体的成员中
struct msgbuf{
long mtype; /* 放消息编号,必须> 0 */
char mtext[msgsz]; /* 消息内容(消息正文) */
};
b过程:
1.调用API时通过“消息队列的标识符”找到对应的消息队列
2.将消息包发送给消息队列,消息包会被作为一个链表节点插入链表
2.接收消息
调用API接收消息时,必须传递两个重要的信息
- 消息队列标识符
- 你要接收消息的编号
有了这两个信息:
API就可以找到
对应的消息队列
,然后从消息队列中取出你所要编号的消息收到了别人所发送的信息,实现了通信
“消息队列”有点像信息公告牌:
--------发送信息的人把某编号的消息挂到公告牌上
--------接收消息的人自己到公告牌上去取对应编号的消息
如此,发送者和接受者之间就实现了通信
----使用消息队列实现网状交叉通信很容易
----消息队列作为媒介,一个往双向链表发数据,一个根据编号取出数据
消息队列使用步骤
-
使用msgget函数
-
消息队列不存在:创建新的消息队列
-
消息队列存在:获取已存在的某个消息队列,并返回唯一标识消息队列的标识符(msqID)
后续收发消息就是使用
msqID
这个标识符来实现的
-
-
收发消息
- 发送消息:使用
msgsnd函数
,利用消息队列标识符发送某编号的消息 - 接收消息:使用
msgrcv函数
,利用消息队列标识符接收某编号的消息
- 发送消息:使用
-
使用
msgctl函数
,利用消息队列标识符删除消息队列
对于使用消息队列来通信的多个进程来说: 只需要一个进程来创建消息队列就可以了 对于其它要参与通信的进程来说: 直接使用这个创建好的消息队列即可
-
为了保证消息队列的创建,让每一个进程都包含创建消息队列的代码,谁先运行就由谁创建
-
后运行的进程如果发现它想用的那个消息队列已经创建好了,就直接使用
-
当众多进程共享操作同一个消息队列时,即可实现进程间的通信。
消息队列API
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//功能:利用key值创建、或者获取一个消息队列
/*
1.如果key没有对应任何消息队列,那就创建一个新的消息队列
2.如果key已经对应了某个消息队列,说明消息队列已经存在了,那就获取这个消息队列来使用
*/
//msgflg:指定创建时的原始权限,比如0664
int msgget(key_t key, int msgflg);
key值--------用于为消息队列生成(计算出)唯一的消息队列ID
-
第一种:指定为IPC_PRIVATE宏
指定这个宏后,每次调用msgget时都会创建一个新的消息队列
-
第二种:可以自己指定一个整形数,但是容易重复指定
-
第三种:使用ftok函数来生成key
#include <sys/types.h> #include <sys/ipc.h> /* ftok通过指定路径名和一个整形数,就可以计算并返回一个唯一对应的key值, ---------只要路径名和整形数不变,所对应的key值就唯一不变的 */ //ftok只会使用整形数(proj_id)的低8位,因此我们往往会指定为一个ASCII码值 key_t ftok(const char *pathname, int proj_id);
创建一个新的消息队列时,除了原始权限,还需要指定IPC_CREAT选项
msgid = msgget(key, 0664|IPC_CREAT);
- 创建一个新的消息队列,此时就会用到msgflg参数
多个进程如何共享同一个消息队列
- 创建进程
- 创建者使用"./file", 'a’生成一个key值
- 然后调用msgget创建了一个消息队列
key = ftok("./file", 'a'); msgid = msgget(key, 0664|IPC_CREAT);
当创建者得到msgid后,即可操作消息队列
- 实现共享
只要能拿到别人创建好的消息队列的ID
,即可共享操作同一个消息队列,实现进程间通信
获取别人创建好的消息队列的ID,有两个方法:
a)创建者把ID保存到某文件,共享进程读出ID即可
这种情况下,共享进程根本不需要调用msgget函数来返回ID
b)调用msgget获取已在消息队列的ID
- 使用ftok函数,利用与创建者相同的“路径名”和8位整形数,生成相同的key值
- 调用msgget函数,利用key找到别人创建好的消息队列,返回ID
key = ftok("./file", 'a'); msgid = msgget(key, 0664|IPC_CREAT);
这种方法是最常用的方法,因为ftok所用到的
“路径名”和“8位的整形数”
比较好记忆
代码演示
创建一个消息队列
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MSG_FIFE "./msgfile"
int create_or_get_msgque(){
//创建消息队列,返回msgid
int msgId=-1;
key_t key=-1;
int fd=0;
//为了用到文件路径名,用open创建
fd=open(MSG_FIFE,O_RDWR|O_CREAT,0664);
//用生成的路径+asc码生成唯一整数
key=ftok(MSG_FIFE,'a');
msgId=msgget(key,0664|IPC_CREAT);
return msgId;
}
int main(void){
int msgID=-1;
int ret=-1;
msgID=create_or_get_msgque();
/*父子进程收发消息*/
ret=fork();
if(ret>0){//父进程发送消息
}
else if(ret==0){//子进程接收消息
}
return 0;
}
ipcs
命令:
- a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
- m:只显示共享内存的信息
- q:只显示消息队列的信息
- s:只显示信号量的信息
system v ipc的缺点:
进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。
如何删除消息队列
- 方法1:重启OS,很麻烦
- 方法2:进程结束时,调用相应的API来删除
- 方法3:使用
ipcrm命令
删除
ipcrm命令
删除共享内存
----+M:按照key值删除 ipcrm -M key
----+m:按照标识符删除 ipcrm -m msgid
删除消息队列
-----+Q:按照key值删除
-----+q:按照标识符删除
删除信号量
-------+S:按照key值删除
-------+s:按照标识符删除
msgsnd()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//功能:将消息挂到消息队列上
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
四个参数:
- msqid:消息队列的标识符
- msgp:存放消息的缓存的地址,类型struct msgbuf类型
这个缓存就是一个消息包(存放消息的结构体变量)
struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[1]; /* message data */ };
-
msgsz:消息正文大小
-
msgflg
- 0:阻塞发送消息(没有发送成功的话,该函数会一直阻塞等)
IPC_NOWAIT
:非阻塞方式发送消息,不管发送成功与否,函数都将返回
msgrcv()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//从消息队列中取出别人所放的某个编号的消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,\
int msgflg);
//返回值
//成功:返回消息正文的字节数
//失败:返回-1,errno被设置
五个参数:
- msqid:消息队列的标识符
- msgp:缓存地址,缓存用于存放所接收的消息
- msgsz:消息正文的大小
- msgtyp:你要接收消息的编号
- msgflg:0:阻塞接收消息;IPC_NOWAIT:非阻塞接收消息
程序—消息队列的通信
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <strings.h>
#define MSG_FIFE "./msgque"
#define MSG_LENGTH 1024
//int msgget(key_t key, int msgflg);
//key_t ftok(const char *pathname, int proj_id);
void print_error(char* str){
perror(str);
exit(-1);
}
int create_or_get_MSG(void){
int fd=0;
int msgid=0;
key_t key=0;
//打开文件,没有就创建
fd=open(MSG_FIFE,O_RDWR|O_CREAT,0664);
if(fd==-1) print_error("open fails\n");
//生成唯一key,用函数ftok()
key=ftok(MSG_FIFE,'a');
if(key==-1) print_error("generate key fails\n");
//消息队列的获取或创建
msgid=msgget(key,0664|IPC_CREAT);//别忘IPC_CREAT
if(msgid==-1) print_error("msgget fails\n");
return msgid;
}
struct msgbuf {//使用时候自己定义
long mtype;
char mtext[MSG_LENGTH];
};
int main(int argc,char** argv){
if(argc!=2) print_error("input two args\n");
int msgID=-1;
msgID=create_or_get_MSG();
long recv_type=0;
recv_type=atol(argv[1]);
pid_t ret=fork();
if(ret>0){//父进程
//int msgsnd(int msqid, const void *msgp,\
size_t msgsz, int msgflg);
struct msgbuf msg_buf={0};
while(1){
bzero(&msg_buf,sizeof(msg_buf));
/*封装消息包*/
printf("输入要通信的内容:\n");
//scanf("%s",msg_buf.mtext);
read(0,msg_buf.mtext,sizeof(msg_buf.mtext));
printf("输入要通信msgbuf的编号\n");
//read(0,&msg_buf.mtype,sizeof(msg_buf.mtype));
scanf("%ld",&msg_buf.mtype);
msgsnd(msgID,&msg_buf,MSG_LENGTH,0);
}
}
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,\
int msgflg);
else if(ret==0){//子进程
struct msgbuf msg_buf={0};
/*接收消息*/
while(1){
int mm=0;
bzero(&msg_buf,sizeof(msg_buf));
mm=msgrcv(msgID,&msg_buf,MSG_LENGTH,recv_type,0);
if(mm>0)
printf("%s\n",msg_buf.mtext);
}
}
return 0;
}
进程结束,自动删除消息队列
msgctl()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//功能是根据cmd指定的要求,去控制消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
进行哪些控制
- 获取消息队列的属性信息
- 修改消息队列的属性信息
- 删除消息队列
调用msgctl函数的最常见目的就是删除消息队列
参数
-
msqid:消息队列标识符
-
cmd
:控制选项其实cmd有很多选项,我这里只简单介绍三个
-
IPC_STAT
:获取属性信息把msqid消息队列的属性信息读到
第三个参数
所指定的缓存
-
IPC_SET
:使用第三个参数中的新设置去修改消息队列的属性IPC_SET
- 定义一个struct msqid_ds buf
- 将新的属性信息设置到buf中
- msgctl函数就会使用buf中的新属性去修改消息队列原有的属性
IPC_RMID
:删除消息队列
-
-
buf:存放属性信息(了解)
struct msqid_ds {
struct ipc_perm msg_perm; /* 消息队列的读写权限和所有者 */
time_t msg_stime; /* 最后一次向队列发送消息的时间*/
time_t msg_rtime; /* 最后一次从消息队列接收消息的时间 */
time_t msg_ctime; /* 消息队列属性最后一次被修改的时间 */
unsigned long __msg_cbytes; /* 队列中当前所有消息总的字节数 */
msgqnum_t msg_qnum; /* 队列中当前消息的条数*/
msglen_t msg_qbytes; /* 队列中允许的最大的总的字节数 */
pid_t msg_lspid; /* 最后一次向队列发送消息的进程PID */
pid_t msg_lrpid; /* 最后一次从队列接受消息的进程PID */
};
//struct ipc_perm msg_perm;
struct ipc_perm {
key_t __key; /* Key supplied to msgget(2):消息队列的key值*/
uid_t uid; /* UID of owner :当前这一刻正在使用消息队列的用户 */
gid_t gid; /* GID of owner :正在使用的用户所在用户组 */
uid_t cuid; /* UID of creator :创建消息队列的用户 */
gid_t cgid; /* GID of creator :创建消息队列的用户所在用户组*/ unsigned short mode; /* Permissions:读写权限(比如0664) */
unsigned short __seq; /* Sequence number :序列号,保障消息队列ID不被立即 重复使用 */};
-
控制1:获取消息队列属性
struct msqid_ds buf;
msgctl(msgid,IPC_STAT
,&
buf); -
控制2:删除消息队列
msgctl(msgid,
IPC_RMID
, NULL);
利用信号捕获,删除消息队列
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#define MSG_FIFE "./msgque"
#define MSG_LENGTH 1024
int msgID=-1;//全局变量
//int msgget(key_t key, int msgflg);
//key_t ftok(const char *pathname, int proj_id);
void print_error(char* str){
perror(str);
exit(-1);
}
int create_or_get_MSG(void){
int fd=0;
int msgid=0;
key_t key=0;
//打开文件,没有就创建
fd=open(MSG_FIFE,O_RDWR|O_CREAT,0664);
if(fd==-1) print_error("open fails\n");
//生成唯一key,用函数ftok()
key=ftok(MSG_FIFE,'a');
if(key==-1) print_error("generate key fails\n");
//消息队列的获取或创建
msgid=msgget(key,0664|IPC_CREAT);//别忘IPC_CREAT
if(msgid==-1) print_error("msgget fails\n");
return msgid;
}
struct msgbuf {//使用时候自己定义
long mtype;
char mtext[MSG_LENGTH];
};
void signal_fun(int signo){
msgctl(msgID,IPC_RMID,NULL);
exit(-1);
}
int main(int argc,char** argv){
if(argc!=2) print_error("input two args\n");
msgID=create_or_get_MSG();
long recv_type=0;
recv_type=atol(argv[1]);
pid_t ret=fork();
if(ret>0){//父进程
//int msgsnd(int msqid, const void *msgp,\
size_t msgsz, int msgflg);
signal(SIGINT,signal_fun);
struct msgbuf msg_buf={0};
while(1){
bzero(&msg_buf,sizeof(msg_buf));
/*封装消息包*/
printf("输入要通信的内容:\n");
//scanf("%s",msg_buf.mtext);
read(0,msg_buf.mtext,sizeof(msg_buf.mtext));
printf("输入要通信msgbuf的编号\n");
//read(0,&msg_buf.mtype,sizeof(msg_buf.mtype));
scanf("%ld",&msg_buf.mtype);
msgsnd(msgID,&msg_buf,MSG_LENGTH,0);
}
}
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,\
int msgflg);
else if(ret==0){//子进程
struct msgbuf msg_buf={0};
/*接收消息*/
while(1){
int mm=0;
bzero(&msg_buf,sizeof(msg_buf));
mm=msgrcv(msgID,&msg_buf,MSG_LENGTH,recv_type,0);
if(mm>0)
printf("%s\n",msg_buf.mtext);
}
}
return 0;
}
- 当你的程序必须涉及到多进程网状交叉通信时,消息队列是上上之选
- 消息队列的缺点,不能实现大规模数据的通信
- 大规模数据的通信,必须使用后面讲的“共享内存”来实现