进程间通信, InterProcess Communication
本质:
内核缓冲区
伪文件 - 不占用磁盘空间
特点:
两部分:
读端,写端,对应两个文件描述符
数据写端流入, 读端流出
操作管道的进程被销毁之后,管道自动被释放了
管道默认是阻塞的。
读写
#### 2 管道的原理
内部实现方式:队列
环形队列
特点:先进先出
缓冲区大小:
默认4k, 大小会根据实际情况做适当调整
队列:
数据只能读取一次,不能重复读取
全双工(Full Duplex)是指在发送数据的同时也能够接收数据,两者同步进行,这好像我们平时打电话一样,说话的同时也能够听到对方的声音。目前的网卡一般都支持全双工。
半双工(Half Duplex)所谓半双工就是指一个时间段内只有一个动作发生,举个简单例子,一条窄窄的马路,同时只能有一辆车通过,当目前有两量车对开,这种情况下就只能一辆先过,等到头儿后另一辆再开,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是基于半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台.
单工通信是指通信线路上的数据按单一方向传送.
匿名管道:适用于有血缘关系的进程
int pipe(int fd[2]);
数组有两个元素, 一个读端一个写端, 各自对应一个文件描述符
例:
#include
#include
#include
#include
#include #include
int main()
{
int fd[2];
// 创建管道
int ret = pipe(fd);
// 如果返回值为-1, 则创建失败
if(ret == -1)
{
perror("pipe error");
exit(1);
}
// 读端
printf("pipe[0] = %d\n", fd[0]);
// 写端
printf("pipe[1] = %d\n", fd[1]);
close(fd[0]);
close(fd[1]);
return 0;
}
思考:
单个进程能否使用管道完成读写操作?
可以
父子进程间通信是否需要sleep函数?
父写 - 写的慢
子读 - 读的快
不用, 因为管道是阻塞的
注意事项
先创建管道, 再创建子进程
父子进程实现 ps aux | grep "bash"
数据重定向: dup2
execlp
#include
#include
#include
#include
#include
#include
int main(int argc, const char* argv[])
{
// 创建管道
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror(“pipe error”);
exit(1);
}
// 创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
// ps aux | grep bash
// 父进程执行 ps aux , 写管道, 关闭读端
if(pid > 0)
{
//关闭读端
close(fd[0]);
// 数据写到管道,STDOUT_FILENO 指向fd[1]的指向,也就是管道的写端
dup2(fd[1], STDOUT_FILENO);
// 执行命令ps -aux
execlp("ps", "ps", "aux", NULL);
// 如果上面失败则:
perror("execlp ps");
exit(1);
}
// 子进程 grep bash 从管道中搜索, 读管道, 关闭写端
else if(pid == 0)
{
// 关掉写端
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("grep", "grep", "bash","--color=auto", NULL);
perror("execlp grep");
exit(1);
}
close(fd[0]);
close(fd[1]);
return 0;
“`
兄弟进程实现ps aux | grep "bash"
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char* argv[])
{
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe error");
exit(1);
}
printf("fd[0] = %d\n", fd[0]);
printf("fd[1] = %d\n", fd[1]);
//创建两个子进程
int i = 0;
int num = 2;
for(; i//如果是子进程则跳出
if(pid == 0)
{
break;
}
}
//父进程回收紫禁城的pcb
if(i == num)
{
close(fd[0]);
close(fd[1]);
//循环回收子进程
//WNOHANG 非阻塞回收
pid_t wpid;
while( (wpid = waitpid(-1, NULL, WNOHANG)) != -1 )
{
if(wpid == 0)
{
continue;
}
printf("died child pid = %d\n", wpid);
}
}
else if(i == 0)
{
// ps aux
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
}
else if(i == 1)
{
// grep bash
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("grep", "grep", "bash", NULL);
}
return 0;
有数据 read(fd)
- 正常读, 返回读出的字节
无数据
可能写端全部关闭
read解除阻塞, 返回0
相当于读文件读到了尾部
没有全部关闭
read阻塞
读端全部被关闭
管道破裂, 进程被中止
内核给当前进程法信号SIGPIPE, 中止进程
读端没有全部关闭
缓冲区写满
write函数阻塞
缓冲区没满
write继续写, 知道满
默认读写两端都阻塞
设置读端为非阻塞pipe(fd)
fcntl - 变参函数
设置方法:
获取原来的flags: int flags = fcntl(fd[0], F_GETFL);
设置新的flags: 非阻塞
flag |= O_NONBLOCK
fcntl(fd[0], F_SETFL, flags);
命令ulimit -a
函数fpathconf
例: long num = fpathconf(fd[0], _PC_PIPE_BUF);
无血缘关系进程间通信
mkfifo 管道名
mkfifo
fifo文件 — myfifo
a.c –> read
close(fd);
b.c — write
int fd1 = open(“myfifo”, O_WRONLY);
write(fd1, “hello”, 5);
close(fd1);
作用: 将磁盘文件的数据映射到内存, 用户通过修改内存就能修改磁盘文件
函数原型
void *mmap
{
void *adrr, //映射区首地址, 传NULL
size_t length, //映射区的大小, 最小4k, 不能为0, 一般和文件同大小
int prot, //映射区权限 PROT_READ--映射区必须要有读权限
int flags, //标志位参数
//MAP_SHARED共享的,数据会同步到磁盘. MAP_PRIVATE私有的,不会同步
int fd, //文件描述符, 需要映射的源文件, 需要先open一个文件
off_t offset //映射文件的指针偏移量, 为4k的整数倍
}
返回值:
成功: 返回映射区的首地址
失败: 返回MAP_FAILED, 就是(void *)-1
函数原型 int munmap(void *addr, size_t length);
addr–映射区的首地址
length–映射区的长度
如修改内存映射区是地址的指针, 则释放失败, 可以复制之后再操作
char *pt = ptr
open的文件的权限应 >= 映射区的权限
父子进程间永远共享:
文件描述符
内存映射区
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char* argv[])
{
//获取文件描述符
int fd = open(“english.txt”, O_RDWR);
if(fd == -1)
{
perror(“open error”);
exit(1);
}
// get file length
// len > 0
int len = lseek(fd, 0, SEEK_END);
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd);
char buf[4096];
// 从内存中读数据
printf("buf = %s\n", (char*)ptr);
strcpy(ptr, "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyaaaaa");
// 进程间通信
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
//父进程
if(pid >0)
{
//写数据
strcpy((char*)ptr, "come on!");
//回收
wait(NULL);
}
else if(pid == 0)
{
//读数据
printf("%s\n", (char*)ptr);
}
// ptr++;
int ret = munmap(ptr, len);
if(ret == -1)
{
perror(“munmap error”);
exit(1);
}
return 0;
“`
匿名映射区:
int main(int argc, const char* argv[])
{
//创建匿名映射区
int len = 4096;
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if(ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// 进程间通信
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
//父进程
if(pid >0)
{
//写数据
strcpy((char*)ptr, "come on!");
//回收
wait(NULL);
}
else if(pid == 0)
{
//读数据
printf("%s\n", (char*)ptr);
}
//释放内存映射区
int ret = munmap(ptr, len);
if(ret == -1)
{
perror("munmap error");
exit(1);
}
return 0;
a.c和b.c, 只能借助磁盘文件创建映射区, 不阻塞
a.c
int fd = open(“hello”);
void* ptr = mmap(, , , ,fd, 0);
写
int main(int argc, char *argv[])
{
int fd = open("temp", O_RDWR | O_CREAT, 0664);
void* ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
while(1)
{
char*p = (char*)ptr;
p += 1024;
strcpy(p, "hello parent, i am your 朋友!!!\n");
sleep(2);
}
// 释放
int ret = munmap(ptr, 4096);
if(ret == -1)
{
perror("munmap");
exit(1);
}
return 0;
}
b.c
int fd1 = open(“hello”);
void* ptr1 = mmap( , , , , fd1, 0);
读
int main(int argc, char *argv[])
{
int fd = open("temp", O_RDWR | O_CREAT, 0664);
ftruncate(fd, 4096);
int len = lseek(fd, 0, SEEK_END);
void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
while(1)
{
sleep(1);
printf("%s\n", (char*)ptr+1024);
}
// 释放
int ret = munmap(ptr, len);
if(ret == -1)
{
perror("munmap");
exit(1);
}
return 0;
}
ipcs
查看共享内存队列消息
ipcrm -m -q -s id
删除对应的内容
shmget()
创建共享内存
int shmget(key_t key, size_t size, int shmflg);
返回值:若成功返回共享内存的ID标识符,错误返回-1,错误原因存于errno
shmat()
– at: attach 挂载内存
void *shmat(int shm_id, const void *shm_addr, int shmflg);
返回值: 若成功返回映射得到的地址,错误返回-1,错误原因存于errno
shmdt()
卸载内存
int shmdt(const void *shmaddr);
返回值: 成功返回0,否则返回-1,错误原因存于errno
shmctl()
控制共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);
shm_id – shmget()函数返回的共享内存标识符
command
*buf – 结构指针, 指向共享内存模式和访问权限的结构
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
msgget
int msgget(key_t key, int msgflg);
与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。
在程序中若要使用消息队列,必须要能知道消息队列key,因为应用进程无法直接访问内核消息队列中的数据结构,因此需要一个消息队列的标识,让应用进程知道当前操作的是哪个消息队列,同时也要保证每个消息队列key值的唯一性。
申请一块内存,创建一个新的消息队列(数据结构msqid_ds),将其初始化后加入到msgque向量表中的某个空位置处,返回标示符。或者在msgque向量表中找键值为key的消息队列。
msgsend
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid是由msgget函数返回的消息队列标识符。
msg_ptr是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样:
struct my_message{
long int message_type;
/* The data you wish to transfer*/
};
msg_sz是msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。
msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.
msgrcv
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msg
msgid, msg_ptr, msg_st的作用也函数msgsnd函数的一样。
msgtype可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。
msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1.
msgctl
int msgctl(int msgid, int command, struct msgid_ds *buf);
command是将要采取的动作,它可以取3个值,
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
IPC_RMID:删除消息队列
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
成功时返回0,失败时返回-1.
简单的例子:
消息发送端: send.c
/*send.c*/
#include
#include
#include
#include
#include
#define MSGKEY 1024
struct msgstru
{
long msgtype;
char msgtext[2048];
};
main()
{
struct msgstru msgs;
int msg_type;
char str[256];
int ret_value;
int msqid;
msqid=msgget(MSGKEY,IPC_EXCL); /*检查消息队列是否存在*/
if(msqid < 0)
{
msqid = msgget(MSGKEY,IPC_CREAT|0666);/*创建消息队列*/
if(msqid <0)
{
printf("failed to create msq | errno=%d [%s]\n",errno,strerror(errno));
exit(-1);
}
}
while (1){
printf("input message type(end:0):");
scanf("%d",&msg_type);
if (msg_type == 0)
break;
printf("input message to be sent:");
scanf ("%s",str);
msgs.msgtype = msg_type;
strcpy(msgs.msgtext, str);
/* 发送消息队列 */
ret_value = msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT);
if ( ret_value < 0 ) {
printf("msgsnd() write msg failed,errno=%d[%s]\n",errno,strerror(errno));
exit(-1);
}
}
msgctl(msqid,IPC_RMID,0); //删除消息队列
}
消息接收端 receive.c
/*receive.c */
#include
#include
#include
#include
#include
#define MSGKEY 1024
struct msgstru
{
long msgtype;
char msgtext[2048];
};
/*子进程,监听消息队列*/
void childproc(){
struct msgstru msgs;
int msgid,ret_value;
char str[512];
while(1){
msgid = msgget(MSGKEY,IPC_EXCL );/*检查消息队列是否存在 */
if(msgid < 0){
printf("msq not existed! errno=%d [%s]\n",errno,strerror(errno));
sleep(2);
continue;
}
/*接收消息队列*/
ret_value = msgrcv(msgid,&msgs,sizeof(struct msgstru),0,0);
printf("text=[%s] pid=[%d]\n",msgs.msgtext,getpid());
}
return;
}
void main()
{
int i,cpid;
/* create 5 child process */
for (i=0;i<5;i++){
cpid = fork();
if (cpid < 0)
printf("fork failed\n");
else if (cpid ==0) /*child process*/
childproc();
}
}
原子操作意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的。
同步:在访问资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问。
同步与互斥是保证在高效率运行的同时,可以正确运行。大部分情况下同步是在互斥的基础上进行的。
不同进程能够看到的一份公共的资源(如:打印机,磁带机等),且一次仅允许一个进程使用的资源称为临界资源。
临界区是一段代码,在这段代码中进程将访问临界资源(例如:公用的设备或是存储器),当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。
相关命令
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
信号量是一个特殊的变量,程序对其访问都是原子操作,为了正确地实现信号量,所以信号量通常是在内核中实现的。,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。
#include
#include
key_t ftok(const char* path, int id);
用来创建一个信号集,或者获取已存在的信号集。
#include
#include
#include
int semget( key_t key, int nsems, int semflg);
操作一个或一组信号, 改变信号量的值, 也可以叫PV操作
#include
#include
#include
int semop(int semid, struct sembuf *sops, size_t nsops);
semid:信号集的识别码,可以通过semget获取。
nsops:信号操作结构的数量,恒大于或等于1.
sops:是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf 结构表示如下:
struct sembuf
{
unsigned short sem_num;
short sem_op;
short sem_flg;
};
sembuf结构体参数说明:
sem_num:操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1, 除非使用一组信息量, 否则它为0。
sem_op:操作信号量
信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作。
sem_flg: 信号操作标识,有如下两种选择:
返回值:成功执行时,都会回0,失败返回-1,并设置errno错误信息。
该函数用来直接控制信号量信息, 用来初始化信号集,或者删除信号集。
#include
#include
#include
int semctl(int semid, int semun, int cmd, ...);
semid:信号量集I P C 标识符。
semun:操作信号在信号集中的编号,第一个信号的号是0.
cmd:在semid指定的信号量集合上执行此命令。
第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union):
union semun
{
int val;
struct semid_ds * buf;
unsigned short * array;
struct seminfo * __buf;
};
返回值:成功返回一个正数,失败返回-1。
详细链接