目录
[进程间通信 IPC](#进程间通信 IPC)
[1. 进程间通信方式](#1. 进程间通信方式)
[2. 无名管道](#2. 无名管道)
[2.1 特点](#2.1 特点)
[2.2 函数接口](#2.2 函数接口)
[2.3 注意事项](#2.3 注意事项)
[3. 有名管道](#3. 有名管道)
[3.1 特点](#3.1 特点)
[3.2 函数接口](#3.2 函数接口)
[3.3 注意事项](#3.3 注意事项)
[3.4 有名管道和无名管道的区别](#3.4 有名管道和无名管道的区别)
[4. 信号](#4. 信号)
[4.3 信号种类](#4.3 信号种类)
[4.4 函数接口](#4.4 函数接口)
[4.4.1 信号发送和挂起](#4.4.1 信号发送和挂起)
[4.4.2 定时器](#4.4.2 定时器)
[4.4.3 信号处理函数signal()](#4.4.3 信号处理函数signal())
[5. 共享内存](#5. 共享内存)
[6. 信号灯集](#6. 信号灯集)
[6.1 特点](#6.1 特点)
[6.3 命令](#6.3 命令)
[6.4 函数接口](#6.4 函数接口)
进程间通信 IPC
InterProcess Communication
1. 进程间通信方式
1)早期的进程间通信:
无名管道(pipe)、有名管道(fifo)、信号(signal)
2)systerm V IPC:
共享内存(share memory)、消息队列(message queue)、信号灯集(semaphore set)
3)BSD:
套接字(socket)
2. 无名管道
2.1 特点
(1) 只能用于具有亲缘关系的进程之间的通信
(2) 半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。
(3) 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
(4) 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
2.2 函数接口
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端 fd[1]:写端
返回值:成功 0
失败 -1
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
char buf[65536] = "";
int fd[2] = {0}; //fd[0]代表读端,fd[1]代表写端
if (pipe(fd) < 0)
{
perror("pipe err");
return -1;
}
printf("%d %d\n", fd[0], fd[1]);
//读写
// write(fd[1], "hello", 32);
// read(fd[0], buf, 32);
// printf("%s\n", buf);
//结构类似队列,先进先出
//1. 当管道中没有数据时,读阻塞。
// read(fd[0], buf, 32);
// printf("%s\n", buf);
//但是关闭写端就不一样
//当管道中有数据,关闭写端可以读出数据来。无数据,关闭写端,读操作会立即返回(返回0)。
//write(fd[1],"hello",5);
// close(fd[1]);
// read(fd[0],buf,5);
// printf("%s\n",buf);
//2.当管道这个写满数据时,写阻塞,管道空间大小是64K
// write(fd[1], buf, 65536);
// printf("write full\n");
// write(fd[1],"a",1); //阻塞,因为管道中被写满了已经。
// printf("write after\n");
//3.写满一次后,当管道中至少有4K空间时(也就是读了4K),才可以继续写,否则阻塞。
//先写满一次再读
// read(fd[0], buf, 4096); //如果读4095后面写就阻塞了,因为不到4K空间。
// write(fd[1], "k", 1);
// printf("write after\n");
//4.当读端关闭,向管道中写入数据无意义,管道破裂,进程会收到内核发送的SIGNAL信号
close(fd[0]);
write(fd[1], "hello", 5);
printf("read close!\n");
return 0;
}
2.3 注意事项
(1) 当管道中无数据的时候,读操作会阻塞。
管道当中有数据,关闭写段,可以将数据读出。
管道中无数据,关闭写段,读操作会立即返回。
(2) 管道中写满时(管道大小64K)写操作会阻塞,写满一次时一旦有4K空间可以继续写。
(3) 只有管道的读端存在时,向管道中写数据才有意义,否则会导致管道破裂,向管道中写入数据进程将收到内核传来的SIGPIPE信号(通常时Broken pipe错误)。
练习:父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入quit结束。
提示:不需要加同步机制, 因为pipe无数据时读会阻塞。
考虑:创建管道是在fork之前还是之后?
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
pid_t pid;
char buf[32] = "";
int fd[2] = {0};
if (pipe(fd) < 0)
{
perror("pipe err");
return -1;
}
printf("%d %d\n", fd[0], fd[1]);
pid = fork();
if (pid < 0)
{
perror("fokr err");
return -1;
}
else if (pid == 0)
{
//循环打印
while (1)
{
//read读管道中内容
read(fd[0], buf, 32);
//判断quit就break
if (strcmp(buf, "quit") == 0)
break;
//printf打印到终端
printf("%s\n", buf);
}
}
else
{
//循环输入
while (1)
{
//先scanf
scanf("%s", buf);
//将写入的buf中内容用write写进管道
write(fd[1], buf, 32);
//write(fd[1], buf, strlen(buf)+1);//+1是为了把\0也写进管道
//判断quit就break
if (strcmp(buf, "quit") == 0)
break;
}
}
//回收子进程
wait(NULL);
return 0;
}
3. 有名管道
3.1 特点
-
有名管道可以使互不相关的两个进程互相通信。
-
有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
-
进程通过文件IO来操作有名管道
-
有名管道遵循先进先出规则
-
不支持如lseek() 操作
3.2 函数接口
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
mode:权限
返回值:成功:0
失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)
注:函数只是在路径下创建管道文件,往管道中写的数据依然写在内核空间。
先创建有名管道,然后用文件IO操作:打开、读写和关闭。
3.3 注意事项
-
只写方式打开阻塞,一直到另一个进程把读打开
-
只读方式打开阻塞,一直到另一个进程把写打开
-
可读可写,如果管道中没有数据,读阻塞
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
int fd;
char buf[32] = "";
if (mkfifo("./fifo", 0666) < 0)
{
if (errno == EEXIST)
printf("file exist\n");
else
{
perror("mkfifo err");
return -1;
}
}
printf("mkfifo success\n");
//打开管道文件
fd = open("./fifo", O_RDWR);
if (fd < 0)
{
perror("open err");
return -1;
}
//读写文件
write(fd, "hello", 5);
read(fd, buf, 5);
printf("%s\n", buf);
return 0;
}
练习:通过两个进程实现cp功能。
./input srcfile
./output destfile
input.c 读源文件
//创建有名管道
//打开管道文件,打开源文件
//循环读源文件,再把内容写道管道
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
int fd_fifo, fd_file;
char buf[32] = "";
ssize_t s;
if (mkfifo("./fifo", 0666) < 0)
{
if (errno == EEXIST)
printf("file exist\n");
else
{
perror("mkfifo err");
return -1;
}
}
printf("mkfifo success\n");
//打开管道文件
fd_fifo = open("./fifo", O_WRONLY);
if (fd_fifo < 0)
{
perror("open fifo err");
return -1;
}
fd_file = open(argv[1], O_RDONLY);
if (fd_file < 0)
{
perror("open file err");
return -1;
}
//读写
while (1)
{
//从文件读到buf,判断读不到就结束
s=read(fd_file,buf,32);
if(s==0)
break;
//把buf中数据写到有名管道中
write(fd_fifo,buf,s);
}
close(fd_fifo);
close(fd_file);
return 0;
}
output.c 写目标文件
//创建有名管道
//打开有名管道,打开目标文件
//循环读管道,把内容写到目标文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
int fd_fifo, fd_file;
char buf[32] = "";
ssize_t s;
if (mkfifo("./fifo", 0666) < 0)
{
if (errno == EEXIST)
printf("file exist\n");
else
{
perror("mkfifo err");
return -1;
}
}
printf("mkfifo success\n");
//打开管道文件
fd_fifo = open("./fifo", O_RDONLY);
if (fd_fifo < 0)
{
perror("open fifo err");
return -1;
}
fd_file = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd_file < 0)
{
perror("open file err");
return -1;
}
//读写
while (1)
{
//从有名管道中读数据到buf,判断
s = read(fd_fifo, buf, 32);
if (s == 0)
break;
//把buf中数据写到目标文件
write(fd_file, buf, s);
}
close(fd_fifo);
close(fd_file);
return 0;
}
3.4 有名管道和无名管道的区别
|------|---------------------------------------------------|-----------------------------------------------------------------|
| | 无名管道 | 有名管道 |
| 使用场景 | 有亲缘关系的进程使用 | 不相干的两个进程使用 |
| 特点 | 半双工通讯方式 固定读端fd[0]和写端fd[1] 看做一种特殊文件,可以通过文件IO操作 | 在文件系统中会存在管道文件,数据存放在内核空间中 通过文件IO进行操作 不支持lseek操作,也遵循先进先出 |
| 函数 | pipe() 直接read、write | mkfifo() 先打开open,再对管道文件read、write读写 |
| 读写特性 | 当管道中无数据,读阻塞 当管道中写满,写阻塞,直到有4K空间才可以写 读端关闭,写会导致管道破裂 | 只读方式打开会阻塞,直到另一个进程把写打开 只写方式打开会阻塞,直到另一个进程把读打开 可读可写,如果管道中没有数据,读阻塞。 |
4. 信号
kill -l: 显示系统中的信号
kill -num PID:给某个进程发送信号
4.1概念
1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
信号的生命周期:
4.2信号的响应方式
1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
3)执行缺省操作:Linux对每种信号都规定了默认操作
4.3 信号种类
SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程
SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件
SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。
SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。
SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination
SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。
SIGCONT(18):继续执行信号,用于恢复先前停止的进程。
SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。
SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。
4.4 函数接口
4.4.1 信号发送和挂起
#include <signal.h>
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
sig:要发送的信号
返回值:成功 0
失败 -1
int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0
失败 -1
相当于:kill(getpid(),sig);
int pause(void);
功能:用于将调用进程挂起,直到收到信号为止。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
// //kill(getpid(), SIGKILL); //给指定进程发送信号,此例子指定当前进程
// raise(SIGKILL); //给自己发信号
// while (1)
// ;
pause(); //将进程挂起,直到收到信号为止,作用类似死循环但是不占用CPU。
printf("hello\n");
return 0;
}
4.4.2 定时器
unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器。当定时器指定的时间到了时,它就向进程发送SIGALARM信号。
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。
常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("%d\n", alarm(10));
sleep(1);
printf("%d\n", alarm(3));
pause(); //将进程挂起,直到收到信号为止,作用类似死循环但是不占用CPU。
return 0;
}
系统默认对SIGALRM(闹钟到点后内核发送的信号)信号的响应: 如果不对SIGALRM信号进行捕捉或采取措施,默认情况下,闹钟响铃时刻会退出进程。例如:
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("%d\n", alarm(3));
while(1); //闹钟响铃后结束进程
return 0;
}
4.4.3 信号处理函数signal()
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
handler:信号处理方式
SIG_IGN:忽略信号 (忽略 ignore)
SIG_DFL:执行默认操作 (默认 default)
handler:捕捉信号 (handler为函数名,可以自定义)
void handler(int sig){} //函数名可以自定义, 参数为要处理的信号
返回值:成功:设置之前的信号处理方式
失败:-1
补充:typedef给数据类型重命名
#include <stdio.h>
//给普通数据类型int重命名
typedef int size4;
//给指针类型int* 重命名
typedef int *int_p;
//给数组类型int [10]重命名
typedef int intArr10[10];
//给函数指针void (*)()重命名
typedef void (*fun_p)();
void fun()
{
printf("fun\n");
}
int main(int argc, char const *argv[])
{
size4 a = 10; //相当于int a
int_p p = &a; //相当于int* p
intArr10 arr = {1, 2, 3}; //相当于int arr[10]
fun_p fp = fun; //相当于void (*pf)();
printf("%d\n", *p);
printf("%d\n", arr[0]);
fp();
return 0;
}
例子:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig)
{
printf("ctrl+C\n");
}
int main(int argc, char const *argv[])
{
//signal(SIGINT,SIG_IGN); //忽略信号
//signal(SIGINT,SIG_DFL); //按默认方式处理信号
signal(SIGINT,handler); //捕捉信号,比较常用的方式
while(1); //为了不让进程结束
return 0;
}
练习:
用信号的知识实现司机和售票员问题。
1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)
2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4)司机等待售票员下车,之后司机再下车。
分析:司机(父进程)、售票员(子进程)
售票员:捕捉:SIGINT SIGQUIT SIGUSR1
忽略:SIGTSTP
司机:捕捉:SIGUSR1 SIGUSR2 SIGTSTP
忽略:SIGINT SIGQUIT
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t pid;
void saler(int sig)
{
if (sig == SIGINT)
kill(getppid(),SIGUSR1);
else if(sig == SIGQUIT)
kill(getppid(),SIGUSR2);
else if(sig == SIGUSR1)
{
printf("pls get off the bus!\n");
exit(0);
}
}
void driver(int sig)
{
if(sig == SIGUSR1)
printf("[driver] let's gogogo!\n");
else if(sig == SIGUSR2)
printf("stop the bus!\n");
else if(sig == SIGTSTP)
{
kill(pid,SIGUSR1);
wait(NULL); //等售票员下车以后再下车
exit(0);
}
}
int main(int argc, char const *argv[])
{
if ((pid = fork()) < 0)
{
perror("fokr err");
return -1;
}
else if (pid == 0)
{
printf("hi,i am saler %d\n", getpid());
signal(SIGINT,saler);
signal(SIGQUIT,saler);
signal(SIGUSR1,saler);
signal(SIGTSTP,SIG_IGN);
}
else
{
printf("hi, i am driver %d\n", getpid());
signal(SIGUSR1,driver);
signal(SIGUSR2,driver);
signal(SIGTSTP,driver);
signal(SIGINT,SIG_IGN);
signal(SIGQUIT,SIG_IGN);
}
while(1)
pause(); //不能只发送一个信号进程就结束了,所以可以循环挂起,不占用CPU。
return 0;
}
5. 共享 内存
5 . 1 特点
1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。
3)进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
4)由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等
5 . 2 步骤
a.创建key值
b.创建或者打开共享内存
c.映射共享内存到用户空间
d.撤销映射
e.删除共享内存
5 . 3 函数 接口
key_t ftok(const char *pathname, int proj_id);
功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存
参数:
Pathname:已经存在的可访问文件的名字
Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
失败:-1
int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
key 键值
size 共享内存的大小
shmflg IPC_CREAT|IPC_EXCL|0777
注意:shmflg为IPC_CREAT|IPC_EXCL|0777这种形式代表创建共享内存,如果只有权限代表打开共享内存
返回值:成功 shmid
出错 -1
void *shmat(int shmid,const void *shmaddr,int shmflg); //attaches
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
shmid 共享内存的id号
shmaddr 一般为NULL,表示由系统自动完成映射
如果不为NULL,那么有用户指定
shmflg:SHM_RDONLY就是对该共享内存只进行读操作
0 可读可写
返回值:成功:完成映射后的地址,
出错:-1(地址)
用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)
int shmdt(const void *shmaddr); //detaches
功能:取消映射
参数:要取消的地址
返回值:成功0
失败的-1
int shmctl(int shmid,int cmd,struct shmid_ds *buf); //control
功能:(删除共享内存),对共享内存进行各种操作
参数:
shmid 共享内存的id号
cmd IPC_STAT 获得shmid属性信息,存放在第三参数
IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
IPC_RMID:删除共享内存,此时第三个参数为NULL即可
buf shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null
返回:成功0
失败-1
用法:shmctl(shmid,IPC_RMID,NULL);
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
int main(int argc, char const *argv[])
{
key_t key;
int shmid;
char *p;
//创建key值,key值由指定文件的inode号和字符的ascii码组合而成
key = ftok("./shm.c", '6');
if (key < 0)
{
perror("ftok err");
return -1;
}
printf("key: %#x\n", key);
//打开或创建共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存
if (shmid <= 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0666); //直接打开已经存在的共享内存
else
{
perror("shmget err");
return -1;
}
}
printf("shmid: %d\n", shmid);
//映射共享内存
p = (char *)shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat err");
return -1;
}
//操作共享内存
strcpy(p, "hello");
printf("%s\n", p);
//取消映射
shmdt(p);
// //删除共享内存
// shmctl(shmid,IPC_RMID,NULL);
return 0;
}
5 . 4 命令
ipcs-m:查看系统中的共享内存
ipcrm-mshmid:删除指定共享内存
注意:可能不能删除还存在进程使用的共享内存,这时候可以用kill杀死多余进程,再使用ipcs查看。
练习:两个进程实现通信,一个进程循环从终端输入,另一个进程循环打印,当输入quit时结束。
提示:为了共享标志位可以和buf封装到一个结构体里作为共享内存。
struct msg
{
int flag;
char buf[32];
};
输入进程:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
typedef struct msg
{
int flag;
char buf[32];
} msg_t;
int main(int argc, char const *argv[])
{
key_t key;
int shmid;
msg_t *p;
//创建key值,key值由指定文件的inode号和字符的ascii码组合而成
key = ftok("./shm.c", '6');
if (key < 0)
{
perror("ftok err");
return -1;
}
printf("key: %#x\n", key);
//打开或创建共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存
if (shmid <= 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0666); //直接打开已经存在的共享内存
else
{
perror("shmget err");
return -1;
}
}
printf("shmid: %d\n", shmid);
//映射共享内存
p = (msg_t *)shmat(shmid, NULL, 0);
if (p == (msg_t *)-1)
{
perror("shmat err");
return -1;
}
p->flag = 0; //初始化
//操作共享内存
while (1)
{
scanf("%s", p->buf);
p->flag = 1;
if (strcmp(p->buf, "quit") == 0)
break;
}
//取消映射
shmdt(p);
// //删除共享内存
// shmctl(shmid,IPC_RMID,NULL);
return 0;
}
输出进程:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
typedef struct msg
{
int flag;
char buf[32];
} msg_t;
int main(int argc, char const *argv[])
{
key_t key;
int shmid;
msg_t *p;
//创建key值,key值由指定文件的inode号和字符的ascii码组合而成
key = ftok("./shm.c", '6');
if (key < 0)
{
perror("ftok err");
return -1;
}
printf("key: %#x\n", key);
//打开或创建共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存
if (shmid <= 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0666); //直接打开已经存在的共享内存
else
{
perror("shmget err");
return -1;
}
}
printf("shmid: %d\n", shmid);
//映射共享内存
p = (msg_t *)shmat(shmid, NULL, 0);
if (p == (msg_t *)-1)
{
perror("shmat err");
return -1;
}
//操作共享内存
p->flag = 0;
while (1)
{
if (strcmp(p->buf, "quit") == 0)
break;
if (p->flag == 1)
{
printf("%s\n", p->buf);
p->flag = 0;
}
}
//取消映射
shmdt(p);
// //删除共享内存
// shmctl(shmid,IPC_RMID,NULL);
return 0;
}
6. 信号灯 集
线程:全局变量,或者通过信号量来实现同步
初始化:sem_init(&sem,0,0);
申请资源:sem_wait(&sem);p操作-1
释放资源:sem_post(&sem);v操作+1
6.1 特点
信号灯(semaphore),也叫信号量,信号灯集是一个信号灯的集合。它是不同进程间或一个给定进程内部不同线程间同步的机制;
而Posix信号灯指的是单个计数信号灯:无名信号灯、有名信号灯。(咱们学的是无名信号灯)
System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。
通过信号灯集实现共享内存的同步操作。
6 . 2 步骤
(1) 创建或者打开信号灯集:semget
(2) 初始化信号灯:semctl
(3) PV操作:semop
(4) 删除信号灯集:semctl
6 .3 命令
ipcs -s查看系统中信号灯集
ipcrm -s semid:删除信号灯集
注意:有时候可能会创建失败,或者semid为0,所以创建完可以用命令看看,如果为0删除了重新创建就可以了。
6 .4 函数 接口
int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
nsems:信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限,通常为IPC_CREAT|IPC_EXCL|0666
返回值:成功:信号灯集ID
失败:-1
int semctl ( int semid, int semnum, int cmd.../*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
semnum: 要操作的集合中的信号灯编号,信号灯编号从0开始
cmd:
GETVAL:获取信号灯的值,返回值是获得值
SETVAL:设置信号灯的值,需要用到第四个参数:共用体
IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
失败 -1
用法:
1. 初始化信号灯集:
需要自定义共用体
union semun{
int val;
} mysemun;
mysemun.val = 10;
semctl(semid, 0, SETVAL, mysemun);
2. 获取信号灯值:函数semctl(semid, 0, GETVAL)的返回值
3. 删除信号灯集:semctl(semid, 0, IPC_RMID);
int semop ( int semid, struct sembuf *opsptr, size_t nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
opsptr:操作方式
nops: 要操作的信号灯的个数 1个
返回值:成功 :0
失败:-1
struct sembuf {
short sem_num; // 要操作的信号灯的编号
short sem_op; // 0 : 等待,直到信号灯的值变成0
// 1 : 释放资源,V操作
// -1 : 申请资源,P操作
short sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};
注意:直接用这个结构体定义变量就可以了,结构体不需要自己写。
用法:
申请资源 P操作:
mysembuf.sem_num = 0;
mysembuf.sem_op = -1;
mysembuf.sem_flg = 0;
semop(semid, &mysembuf, 1);
释放资源 V操作:
mysembuf.sem_num = 0;
mysembuf.sem_op = 1;
mysembuf.sem_flg = 0;
semop(semid, &mysembuf, 1);
创建 和 使用 信号灯 :
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/sem.h>
#include <errno.h>
union semun
{
int val;
};
int main(int argc, char const *argv[])
{
int semid;
key_t key;
if ((key = ftok("./sem.c", 66)) < 0)
{
perror("ftok err");
return -1;
}
printf("key: %#x\n", key);
//创建信号灯集
semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == EEXIST)
semid = semget(key, 2, 0666); //直接打开信号灯集
else
{
perror("semget err");
return -1;
}
}
else //确保对信号灯集中的信号灯只初始化一次,下次执行程序还继续获得上一次的值。如果想修改初值,需要删除信号灯然后重新打开。
{
union semun sem;
sem.val = 10;
semctl(semid,0,SETVAL,sem); //对编号为0的信号灯初值设为10
sem.val = 0;
semctl(semid,1,SETVAL,sem); //对编号为1的信号灯初值设为0
}
printf("semid: %d\n", semid);
//获取信号灯初值
printf("%d\n",semctl(semid,0,GETVAL)); //获取编号为0的信号灯的值
printf("%d\n",semctl(semid,1,GETVAL)); //获取编号为1的信号灯的值
//pv操作
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1; //申请资源,P操作,-1
buf.sem_flg=0; //阻塞
semop(semid,&buf,1);
buf.sem_num=1;
buf.sem_op=1; //释放资源,V操作,+1
buf.sem_flg=0;
semop(semid,&buf,1);
printf("%d\n",semctl(semid,0,GETVAL)); //获取编号为0的信号灯的值
printf("%d\n",semctl(semid,1,GETVAL)); //获取编号为1的信号灯的值
// //删除信号灯集
// semctl(semid,0,IPC_RMID); //指定任意一个信号灯就可以,会删除整个信号灯集。
return 0;
}
函数操作:
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/sem.h>
#include <errno.h>
union semun {
int val;
};
void seminit(int semid, int num, int val) //初始化
{
union semun sem;
sem.val = val;
semctl(semid, num, SETVAL, sem); //对编号为num的信号灯初值设为val
}
void sem_op(int semid, int num,int op)
{
struct sembuf buf;
buf.sem_num = num;
buf.sem_op = op;
buf.sem_flg =0;
semop(semid,&buf,1);
}
int main(int argc, char const *argv[])
{
int semid;
key_t key;
if ((key = ftok("./sem.c", 66)) < 0)
{
perror("ftok err");
return -1;
}
printf("key: %#x\n", key);
//创建信号灯集
semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == EEXIST)
semid = semget(key, 2, 0666); //直接打开信号灯集
else
{
perror("semget err");
return -1;
}
}
else //确保对信号灯集中的信号灯只初始化一次,下次执行程序还继续获得上一次的值。如果想修改初值,需要删除信号灯然后重新打开。
{
seminit(semid, 0, 10);
seminit(semid, 1, 0);
}
printf("semid: %d\n", semid);
//获取信号灯初值
printf("%d\n", semctl(semid, 0, GETVAL)); //获取编号为0的信号灯的值
printf("%d\n", semctl(semid, 1, GETVAL)); //获取编号为1的信号灯的值
//pv操作
sem_op(semid,0,-1);
sem_op(semid,1,1);
printf("%d\n", semctl(semid, 0, GETVAL)); //获取编号为0的信号灯的值
printf("%d\n", semctl(semid, 1, GETVAL)); //获取编号为1的信号灯的值
// //删除信号灯集
// semctl(semid,0,IPC_RMID); //指定任意一个信号灯就可以,会删除整个信号灯集。
return 0;
}
把 信号灯 集 加到 共享 内存 :
输入功能程序:
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/sem.h>
#include <errno.h>
#include <string.h>
#include <sys/shm.h>
union semun {
int val;
};
void seminit(int semid, int num, int val) //初始化
{
union semun sem;
sem.val = val;
semctl(semid, num, SETVAL, sem); //对编号为num的信号灯初值设为val
}
void sem_op(int semid, int num, int op)
{
struct sembuf buf;
buf.sem_num = num;
buf.sem_op = op;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
int semid;
int shmid;
char *p;
key_t key;
if ((key = ftok("./sem.c", 66)) < 0)
{
perror("ftok err");
return -1;
}
printf("key: %#x\n", key);
//创建/打开共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid <= 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0666);
else
{
perror("shmget err");
return -1;
}
}
printf("shmid: %d\n", shmid);
//创建信号灯集
semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == EEXIST)
semid = semget(key, 2, 0666); //直接打开信号灯集
else
{
perror("semget err");
return -1;
}
}
else //确保对信号灯集中的信号灯只初始化一次,下次执行程序还继续获得上一次的值。如果想修改初值,需要删除信号灯然后重新打开。
{
seminit(semid, 0, 0);
}
printf("semid: %d\n", semid);
//共享内存映射
p = (char *)shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat err");
return -1;
}
while (1)
{
scanf("%s", p);
//释放资源
sem_op(semid,0,1);
if (strcmp(p, "quit")==0)
break;
}
shmdt(p);
shmctl(shmid,IPC_RMID,NULL);
// //删除信号灯集
// semctl(semid,0,IPC_RMID); //指定任意一个信号灯就可以,会删除整个信号灯集。
return 0;
}
out.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/sem.h>
#include <errno.h>
#include <string.h>
#include <sys/shm.h>
union semun {
int val;
};
void seminit(int semid, int num, int val) //初始化
{
union semun sem;
sem.val = val;
semctl(semid, num, SETVAL, sem); //对编号为num的信号灯初值设为val
}
void sem_op(int semid, int num, int op)
{
struct sembuf buf;
buf.sem_num = num;
buf.sem_op = op;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
int semid;
int shmid;
char *p;
key_t key;
if ((key = ftok("./sem.c", 66)) < 0)
{
perror("ftok err");
return -1;
}
printf("key: %#x\n", key);
//创建/打开共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
if (shmid <= 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0666);
else
{
perror("shmget err");
return -1;
}
}
printf("shmid: %d\n", shmid);
//创建信号灯集
semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == EEXIST)
semid = semget(key, 2, 0666); //直接打开信号灯集
else
{
perror("semget err");
return -1;
}
}
else //确保对信号灯集中的信号灯只初始化一次,下次执行程序还继续获得上一次的值。如果想修改初值,需要删除信号灯然后重新打开。
{
seminit(semid, 0, 0);
}
printf("semid: %d\n", semid);
//共享内存映射
p = (char *)shmat(shmid, NULL, 0);
if (p == (char *)-1)
{
perror("shmat err");
return -1;
}
while (1)
{
sem_op(semid,0,-1); //申请资源
if (strcmp(p, "quit")==0)
break;
printf("%s\n", p);
}
shmdt(p);
shmctl(shmid, IPC_RMID, NULL);
// //删除信号灯集
// semctl(semid,0,IPC_RMID); //指定任意一个信号灯就可以,会删除整个信号灯集。
return 0;
}
7 . 消息 队列 : m e s s a g e q u e u e
传统:无名管道、有名管道、信号
systemV:共享内存、信号灯集、消息队列
按消息的类型添加或读取消息
队列原则
7 . 1 特点
消息队列是IPC对象(活动在内核级别的一种进程间通信的工具)的一种
一个消息队列由一个标识符 (即队列ID)来标识
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
消息队列可以按照类型(自己设一个值作为类型)来发送/接收消息
7 . 2 步骤
(1) 产生key值ftok
(2) 创建或打开消息队列msgget()
(3) 添加消息:按照消息的类型添加到已经打开的消息队列的末尾msgsnd()
(4) 读取消息:可以按照消息的类型从消息队列中读走msgrcv()
(5) 删除消息队列:msgctl()
7 . 3 命令
ipcs -q:查看系统中的消息队列
ipcrm -q msgid:删除消息队列
7 . 4 函数 接口
int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数: key值
flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
失败:-1
int msgsnd(int msqid, const void *msgp, size_t size, int flag);
功能:添加消息
参数:msqid:消息队列的ID
msgp:指向消息的指针。常用消息结构msgbuf如下:
struct msgbuf{
long mtype; //消息类型
char mtext[N]}; //消息正文
size:发送的消息正文的字节数
flag:IPC_NOWAIT消息没有发送完成函数也会立即返回
0:直到发送完成函数才返回
返回值:成功:0
失败:-1
使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。
int msgrcv(int msgid, void* msgp, size_t size, long msgtype, int flag);
功能:读取消息
参数:msgid:消息队列的ID
msgp:存放读取消息的空间
size:接受的消息正文的字节数(sizeof(msgp)-sizeof(long))
msgtype:
0:接收消息队列中第一个消息。
大于0:接收消息队列中第一个类型为msgtyp的消息.
小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
flag:
0:若无消息函数会一直阻塞
IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
失败:-1
int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,删除消息队列
参数:msqid:消息队列的队列ID
cmd:
IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
IPC_SET:设置消息队列的属性。这个值取自buf参数。
IPC_RMID:从系统中删除消息队列。
buf:消息队列缓冲区
返回值:成功:0
失败:-1
用法:msgctl(msgid, IPC_RMID, NULL)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <errno.h>
struct msgbuf
{
long type; //必须有而且必须是long类型,表示消息类型,值>0
int num; //正文随便定义
char ch;
};
int main(int argc, char const *argv[])
{
key_t key;
int msgid;
key = ftok("msg.c", 99);
if (key < 0)
{
perror("ftok err");
return -1;
}
printf("key:%#x\n", key);
//打开消息队列
msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid <= 0)
{
if (errno == EEXIST)
msgid = msgget(key, 0666);
else
{
perror("msgget err");
return -1;
}
}
printf("msgid:%d\n", msgid);
//添加消息
struct msgbuf msg;
msg.type = 10;
msg.num = 100;
msg.ch = 'a';
msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0); //0:阻塞等发完才返回
msg.type = 20;
msg.num = 200;
msg.ch = 'b';
msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);
//读取消息
struct msgbuf m;
msgrcv(msgid, &m, sizeof(m) - sizeof(long), 20, 0); //读取消息队列中类型为20的第一个消息
printf("%d %c\n", m.num, m.ch);
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
两个进程分别发和收: