目录
IPC(Inter-Process Communication,进程间通信)是指至少两个进程或线程间传送数据或信号的一些技术或方法。在操作系统中,进程是资源分配的基本单位,每个进程都有自己的一部分独立的系统资源,彼此是隔离的。有时,进程间需要进行数据交换,这时就需要用到IPC机制。LINUX通信方式主要包括:管道、信号、共享内存、消息队列以及信号量。
一、管道
1、无名管道
1.1 定义
无名管道只能在具有亲缘关系的进程间(如父子进程或兄弟进程)使用。管道也有两个端口,分别为读端口和写端口。管道的进水端可以理解为写入端口,出水端就读取数据的端口。
第一种:单进程的读写过程
第二种:父子进程读写过程
通过上面可以发现,管道里面的数据,只能朝着一个方向传递,一个进程发送数据的时候,另外一个进程只能接收数据。
1.2 特点
①只能在亲缘关系的进程间(父子或兄弟)进行通信
②半双工(固定的读端口和固定的写端口)
③特殊文件,可以通过read/write对管道进行读写,管道只能存在内存中。
④无名管道没有文件名,也没有对应的文件节点。
说明:文件节点是文件系统中的一个数据结构,用于存储文件的数据(如权限、所有者、 大小等)以及指向文件数据的指针。对于无名管道来说,它们并不存在于文件系统 中,而是直接在内核的内存中创建的。这意味着它们没有与文件系统相关联的文件 名或文件节点。相反,无名管道是通过文件描述符来访问的,这些文件描述符是由 pipe系统调用返回的,并且只能在创建它们的进程及其子进程(或具有某种亲缘关 系的进程)之间共享。由于无名管道没有文件名和文件节点,因此它们不能通过常 规的文件操作(如open、read、write等,除非是通过文件描述符)来访问。相反, 它们必须使用专门的系统调用(如pipe、fork、exec等)来创建和使用。
⑤管道具有阻塞的特性:当缓冲区满时,写操作会阻塞,直到有空间可用;当缓冲区为空时,读操作会阻塞,直到有数据可读。
⑥管道具有队列结构,特性先进先出FIFO(类似:DMA),放入管道的数据被接收,那么管道里面的数据被移除。
1.3通信流程
无名管道操作步骤:
创建管道-->创建父子进程-->利用无名管道进行父进程与子进程之间的数据交换-->摧毁无名管道
1.4 相关函数
pipe
功能:
用于在进程间创建一个无名管道,并且这个管道是一个单向的、固定大小的缓冲区,在具有亲缘关系(通常指父子进程)的进程间传递数据。
头文件:
#include <unistd.h>
函数原型:
int pipe(int pipefd[2]);
函数参数:
int pipefd[2]:pipe函数创建一个无名管道,将对应管道的fd—是两个端口,放到数组里
pipefd[0]--->表示的是管道的读端口 pipefd[1]--->表示的是管道的写端口
函数返回值:
成功 返回0,并且pipefd数组被填充为两个有效的文件描述符
失败 -1
示例1:利用一下pipe函数,创建一个无名管道
现象:
代码:
#include <unistd.h>
int main(int argc,char *argv[])
{
int pipefd[2];
//创建管道
int ret = pipe(pipefd);
if(ret == 0)
{
printf("pipefd[0]=%d\r\n",pipefd[0]);
printf("pipefd[1]=%d\r\n",pipefd[1]);
}
return 0;
}
示例2:利用单进程操作无名管道,实现数据写入和读取
现象:
代码:
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
int pipefd[2];
char buff_w[] = "hello";
char buff_r[8];
//创建管道
int ret = pipe(pipefd);
if(ret == 0)
{
printf("pipefd[0]=%d\r\n",pipefd[0]);
printf("pipefd[1]=%d\r\n",pipefd[1]);
}
//写入数据
int res1 = write(pipefd[1],buff_w,sizeof(buff_w));
printf("res1=%d\r\n",res1);
//读取数据
int res2 = read(pipefd[0],buff_r,sizeof(buff_r));
printf("res2=%d\r\n",res2);
//关闭管道
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
示例3:利用父子进程,操作对应的无名管道,实现一次通信
现象:
代码:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc,char argv[])
{
int ret;
int pipefd[2];
//创建管道
ret = pipe(pipefd);
if(ret == 0)
{
printf("pipe success\r\n");
printf("pipefd[0]:%d\r\n",pipefd[0]);
printf("pipefd[1]:%d\r\n",pipefd[1]);
}
//创建父子进程
ret=fork();
if(ret == 0)//子进程
{
char buf[10];
//接收数据
read(pipefd[0],buf,sizeof(buf));
printf("buf:%s\r\n",buf);
}
else//父进程
{
char str[10];
scanf("%[^\n]",str);
getchar();
//发送数据
write(pipefd[1],str,sizeof(str));
printf("send success\r\n");
}
return 0;
}
示例4:利用父子进程,操作无名管道实现,交替通信
现象:
代码:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc,char argv[])
{
int ret;
int pipefd[2];
char str[10];
char buf[10];
//创建管道
ret = pipe(pipefd);
if(ret == 0)
{
printf("pipe success\r\n");
printf("pipefd[0]:%d\r\n",pipefd[0]);
printf("pipefd[1]:%d\r\n",pipefd[1]);
}
//创建父子进程
ret=fork();
if(ret == 0)//子进程
{
while(1)
{
//接收数据
read(pipefd[0],buf,sizeof(buf));
printf("child_buf:%s\r\n",buf);
//获取需要发送的数据
scanf("%[^\n]",str);
getchar();
//发送数据
write(pipefd[1],str,sizeof(str));
printf("child send success\r\n");
//阻塞
sleep(1);
}
}
else//父进程
{
while(1)
{
//获取需要发送的数据
scanf("%[^\n]",str);
getchar();
//发送数据
write(pipefd[1],str,sizeof(str));
printf("parent send success\r\n");
//阻塞
sleep(1);
//接收数据
read(pipefd[0],buf,sizeof(buf));
printf("parent_buf:%s\r\n",buf);
}
}
return 0;
}
2、命名管道
2.1 定义
命名管道,也被称为FIFO,是一种特殊的文件类型,它允许在文件系统中通过路径名访问,从而实现非亲缘进程之间的通信。命名管道在文件系统中有一个对应的路径名,因此不同进程可以通过这个路径名来访问同一个管道。它提供了单向的数据流,即数据只能在一个方向上流动。如果需要双向通信,则需要在两端各建立一个命名管道。
2.2 特点
①适用于任意两个进程之间的通信。
②它是特殊文件(.fifo),可以使用read\write函数进行读写,并只能在内存中运行。
③命名管道具有文件名和文件节点。--其他进程可以使用管道的名字进程操作
④管道具有阻塞的特性:当缓冲区满时,写操作会阻塞,直到有空间可用;当缓冲区为空时,读操作会阻塞,直到有数据可读。
⑤管道具有队列结构,先进先出功能。
⑥ 打开命名管道,以O_RDONLY打开的方式,默认情况是读端口。
打开命名管道,以O_WRONLY打开的方式,默认情况是写端口。
一般情况下,打开命名管道,以O_RDWR方式打开-->自适应读端口或写端口
2.3 通信流程
命名管道操作步骤:
创建管道(获得对应的fd)-->打开管道文件-->利用命名管道进行父进程与子进程之间的数据交换-->摧毁命名管道
2.4 相关函数
mkfifo
函数功能:
用于在进程间创建一个管道文件,允许无亲缘关系进程间通信。
头文件:
#include <sys/types.h>
#include <sys/stat.h>
函数原型:
int mkfifo(const char *pathname,mode_t mode);
函数参数:
const char *pathname:文件的路径名
mode_t mode:期望创建管道文件的权限
函数返回值:
成功 返回0
失败 -1
示例1:利用mkfifo函数创建命名管道
现象:
代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
//创建管道
int ret = mkfifo("c.fifo",0666);
if(ret == 0)
{
printf("pipe success\r\n");
}
return 0;
}
示例2:利用命名管道实现单进程的通信过程
现象:
代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
int fd;
char buf[10];
int ret = mkfifo("c.fifo",0666);
if(ret == 0)
{
printf("pipe success\r\n");
}
//打开管道文件
fd = open("c.fifo",O_RDWR);
printf("fd=%d\r\n",fd);
//往对应的命名管道写入数据
write(fd,"hello",sizeof("hello"));
//从出口处获取数据
read(fd,buf,sizeof(buf));
printf("buf:%s\r\n",buf);
//关闭对应的文件描述符
close(fd);
return 0;
}
unlink
函数功能:
删除对应的管道文件
头文件:
#include <unistd.h>
函数原型:
int unlink(const char *pathname);
函数参数:
const char *pathname:表示待删除命名管道文件的路径
函数返回值:
成功 返回0
失败 -1
示例:利用一下unlink函数,将对应的命名管道删除掉,观察对应的现象
现象:
代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
int fd;
char buf[10];
int ret = mkfifo("c.fifo",0666);
if(ret == 0)
{
printf("pipe success\r\n");
}
//打开管道文件
fd = open("c.fifo",O_RDWR);
printf("fd=%d\r\n",fd);
//往对应的命名管道写入数据
write(fd,"hello",sizeof("hello"));
//从出口处获取数据
read(fd,buf,sizeof(buf));
printf("buf:%s\r\n",buf);
//删除对应的管道
ret = unlink("c.fifo");
if(ret == 0)
{
printf("unlink success\r\n");
}
//关闭对应的文件描述符
close(fd);
return 0;
}
remove
函数功能:
删除对应的文件(包括管道文件、文件夹)
头文件:
#include <stdio.h>
函数原型:
int remove(const char *pathname)
函数参数:
const char *pathname:表示待删除文件的路径
函数返回值:
成功 返回0
失败 -1
示例:调用一下remove函数,观察对应的现象
现象:
代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
int fd;
char buf[10];
int ret = mkfifo("c.fifo",0666);
if(ret == 0)
{
printf("pipe success\r\n");
}
//打开管道文件
fd = open("c.fifo",O_RDWR);
printf("fd=%d\r\n",fd);
//往对应的命名管道写入数据
write(fd,"hello",sizeof("hello"));
//从出口处获取数据
read(fd,buf,sizeof(buf));
printf("buf:%s\r\n",buf);
//删除对应的管道
ret = remove("c.fifo");
if(ret == 0)
{
printf("remove success\r\n");
}
//关闭对应的文件描述符
close(fd);
return 0;
}
2.5 综合实例
综合1:利用父子进程,操作命名管道,进行通信的过程。
现象:
代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
int ret,fd;
char str[10],buf[10];
//创建管道
ret = mkfifo("c.fifo",0666);
if(ret == 0)
{
printf("mkfifo success\r\n");
}
//打开管道文件
fd = open("c.fifo",O_RDWR);
//创建父子进程
ret=fork();
if(ret == 0)//子进程
{
while(1)
{
//接收数据
read(fd,buf,sizeof(buf));
printf("child_buf:%s\r\n",buf);
//获取需要发送的数据
scanf("%[^\n]",str);
getchar();
//发送数据
write(fd,str,sizeof(str));
printf("child send success\r\n");
//阻塞
sleep(1);
}
}
else//父进程
{
while(1)
{
//获取需要发送的数据
scanf("%[^\n]",str);
getchar();
//发送数据
write(fd,str,sizeof(str));
printf("parent send success\r\n");
//阻塞
sleep(1);
//接收数据
read(fd,buf,sizeof(buf));
printf("parent_buf:%s\r\n",buf);
}
}
//等待子进程运行结束
wait(NULL);
//删除对应的管道
ret = remove("c.fifo");
if(ret == 0)
{
printf("remove success\r\n");
}
//关闭对应的文件描述符
close(fd);
return 0;
}
综合2:利用命名管道,非亲缘关系进程实现—自由通信
现象:
代码:
进程A
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
int ret,fd1,fd2;
char str[10],buf[10];
//创建管道1
ret = mkfifo("a.fifo",0666);
if(ret == 0)
{
printf("mkfifo1 success\r\n");
}
//创建管道2
ret = mkfifo("b.fifo",0666);
if(ret == 0)
{
printf("mkfifo2 success\r\n");
}
//打开管道文件
fd1 = open("a.fifo",O_RDWR);
fd2 = open("b.fifo",O_RDWR);
//创建父子进程
ret=fork();
if(ret == 0)//子进程
{
while(1)
{
//接收数据
read(fd2,buf,sizeof(buf));
printf("child_buf:%s\r\n",buf);
}
}
else//父进程
{
while(1)
{
//获取需要发送的数据
scanf("%[^\n]",str);
getchar();
//发送数据
write(fd1,str,sizeof(str));
printf("parent send success\r\n");
}
}
//等待子进程运行结束
wait(NULL);
//删除对应的管道
ret = remove("a.fifo");
if(ret == 0)
{
printf("remove success\r\n");
}
//关闭对应的文件描述符
close(fd1);
close(fd2);
return 0;
}
进程B
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
int ret,fd1,fd2;
char str[10],buf[10];
//打开管道文件
fd1 = open("a.fifo",O_RDWR);
fd2 = open("b.fifo",O_RDWR);
//创建父子进程
ret=fork();
if(ret == 0)//子进程
{
while(1)
{
//接收数据
read(fd1,buf,sizeof(buf));
printf("child_buf:%s\r\n",buf);
}
}
else//父进程
{
while(1)
{
//获取需要发送的数据
scanf("%[^\n]",str);
getchar();
//发送数据
write(fd2,str,sizeof(str));
printf("parent send success\r\n");
}
}
//等待子进程运行结束
wait(NULL);
//删除对应的管道
ret = remove("b.fifo");
if(ret == 0)
{
printf("remove success\r\n");
}
//关闭对应的文件描述符
close(fd1);
close(fd2);
return 0;
}
注意:非亲缘关系的进程,操作命名管道的时候。某个时刻,进程A和进程B都没有操作命名管道,管道里面的数据将会消失。
2.6 补充说明
无名管道与命名管道的区别
①创建方式与存在位置
无名管道:
由操作系统内核自动创建,无需显式地在文件系统中创建文件;不存在于任何文件系统中,只存在于内存中
命名管道:
需要在文件系统中创建一个特殊类型的文件(FIFO文件),以提供一个命名管道的路径;存在于文件系统中,并有一个与之关联的文件路径名②使用范围与进程关系
无名管道:
只能用于具有亲缘关系的进程之间的通信,如父子进程或兄弟进程;由于无名管道不存在于文件系统中,因此无法被不相关的进程访问。
命名管道:
可以被多个进程同时使用,即多个读取或写入进程可以通过指定相同的路径来进行通信;适用于任意多个进程之间的通信,无论这些进程是否具有亲缘关系。③通信模式与方向
无名管道:
是单工的通信模式,数据只能单向发送。即数据只能从管道的一端写入,从另一端读出,有固定的读端和写端。
命名管道:
可以支持双向通信(两个进程进行自由通信->两个管道),两个进程也可以通过同一管道进行交互(半双工)
文件系统与内存之间的交互
文件读取:
当进程需要读取文件中的数据时,操作系统会将文件的部分或全部内容加载到内存中,形成一个内存缓冲区。
进程通过访问内存缓冲区来读取文件数据,而不是直接访问存储设备。
文件写入:
当进程需要向文件中写入数据时,操作系统通常会将数据先写入内存中的一个缓冲区。
当缓冲区满或进程显式地要求将数据写入存储设备时,操作系统会将缓冲区中的数据写入文件系统对应的存储设备中。
3、标准流管道
3.1 定义
标准流管道---标准IO有关的,文件流指针---FILE *fp --头文件:#include <stdio.h>,Linux文件中的标准IO操作方法,都是管道流文件操作一种方式。
3.2 相关函数
popen
函数功能:
用于创建一个管道连接到另一个进程,通常用于执行一个命令,并且能够从该命令读取输出或者向该命令写入输入
头文件:
#include <stdio.h>
函数原型:
FILE *popen(const char *command, const char *type);
函数参数:
const char *command:表示需要在子进程中执行的命令
const char *type :“r” 可以用于返回文件的指针,读取命令的输出;“w" 可以用于返回文件的指针,向命令写入输入
函数返回值:
成功 返回文件流指针--->第二个参数决定的
失败 返回-1
注意:
使用 popen() 函数可以简化管道流操作,它结合了管道创建、进程创建、文件描述符管理和命令执行等多个步骤。具体执行步骤:
①创建一个管道。
②fork()函数创建子进程。
③在父子进程中关闭不需要使用的文件描述符。
④执行execl()函数
⑤执行函数中指定的命令—command。
特点:调用的函数大大减少,减少了代码的数量,不需要自己进行创建,并注意popen()必须使用标准IO进行操作—返回对应的文件流指针fp。例如:“fread”“fwrite”,不能使用文件IO里面的read、write,必须使用pclose()函数关闭标准流管道。
pclose
函数功能:
关闭管道文件
头文件:
#include <stdio.h>
函数原型:
int pclose(FILE *stream);
函数参数:
FILE *stream:文件流指针
函数返回值:
返回子进程结束的状态
示例1:利用popen函数,使用命令ls,观察对应的现象
现象:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
//直接调用对应的popen
FILE *fp;
char buf[40]={0};
fp=popen("ls", "r");
if(fp != NULL)
{
printf("popen success\r\n");
}
//读取对应fp里面的内容
fread(buf,sizeof(char),sizeof(buf),fp);
//输出一下buf里面的内容
printf("buf=%s",buf);
//调用对应的pclose函数
pclose(fp);
return 0;
}
执行流程:
创建管道和子进程:
在 main() 函数中,调用 popen("ls", "r")。
popen() 函数内部首先创建一个管道,用于父子进程之间的通信。
接着,popen() 调用 fork() 创建一个子进程。
子进程执行命令:
在子进程中,popen() 函数重定向子进程的标准输出(stdout)到管道的写端。
子进程执行 ls 命令,并将输出写入到管道中。
子进程执行完毕后(在输出完所有内容后),它会关闭管道的写端并退出。
父进程读取输出:
在父进程中,popen() 返回一个指向 FILE 结构体的指针 fp,该指针与管道的读端相关联。父进程使用 fread(buf, 1, sizeof(buf) , fp) 从管道中读取数据到缓冲区 buf 中。fread() 会持续读取数据,直到达到缓冲区大小限制、遇到文件结束符(EOF),或发生错误。
处理读取的数据:
父进程在读取完数据后,手动在缓冲区末尾添加 null 终止符 '\0',以确保字符串正确终止。然后,父进程使用 printf() 函数将缓冲区中的内容打印到标准输出上。
关闭管道和清理资源:
父进程调用 pclose(fp) 关闭与管道相关联的文件指针,并等待子进程结束(如果子进程还没有结束的话)。pclose() 函数内部会关闭管道的读端,并处理任何必要的清理工作。
如果子进程已经结束,pclose() 会返回子进程的退出状态。
示例2:利用popen函数,写操作---关键:找到一个可以写的命令 cat > 1.txt (可以利用键盘写到1.txt文件里面的)
现象:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
FILE *fp;
char buf[10];
//创建管道
fp = popen("cat > 1.txt", "w");
if(fp != NULL)
{
printf("popen success\r\n");
}
scanf("%s",buf);
//往对应的fp里面写入数据
fwrite(buf,sizeof(char),sizeof(buf),fp);
//调用对应的pclose函数
pclose(fp);
}
示例3:利用popen函数—调用对应“r”,调用一个命令:cat a.c –功能:将对应a.c这个源文件里面的代码,显示到屏幕上(输出内容)
现象:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
FILE *fp;
int res;
char buff[2];
//创建管道
fp = popen("cat a.c", "w");
if(fp != NULL)
{
printf("popen success\r\n");
}
while(1)
{
//读取数据
res = fread(buff,sizeof(char),1,fp);
if(res == 0)
{
break;
}
//显示到屏幕
fwrite(buff,sizeof(char),1,stdout);
//清数组
memset(buff,0,sizeof(buff));
}
//调用对应的pclose函数
pclose(fp);
}
二、信号
1、信号的本质
Linux系统中信号相当于软中断(本质),用来通知异步事件的发生,软件层上对中断的一种模拟。在原理上,一个进程接收到一个信号,和处理器接受到一个中断请求(硬中断)可以说是一样的。信号是进程通信的一种异步通信机制,一个进程不必通过任何操作等待信号到来,实际上,进程也不知道什么时候信号会达到。
一旦进程A,接收到了信号,中断当前进程的执行,进入到对应的信号处理函数里面,执行对应的代码;在软中断处理完毕后,系统会检查之前保存的现场信息,并根据这些信息恢复到被中断的进程的执行点。
2、信号的来源
信号的来源可以分为多种,按照产生条件的不同可以分为两种--->硬件和软件
第一种:硬件方式
①按键ctrl+c-->发送SIGINT信号(2)给前台进程组中的所有的进程,这些正在运行的进程接收到信号,终止运行。会终止当前进程的执行。也可以改变对应信号的处理方式
②按键ctrl+z-->发送SIGSTOP信号(19)给其他进程组中的所有进程,挂起进程
③按键ctrl+\-->发送SIGTTOU信号(22)给前台进程组中的所有进程,终止进程并生成core文件
第二种:软件方式
使用命令:kill 发送信号(内部实现就是通过kill函数)-->把某个进程杀死(可以在终端发送信号)
使用函数:kill() ,sigqueue()发送信号
3、信号的分类
从不同的角度将信号进行分类:
可靠性方面:可靠信号与非可靠信号。
与时间的关系:实时信号与非实时信号。
=========================================================================
可靠信号与不可靠信号的来源
UNIX系统早期系统比较简单,信号可能丢失,对这一类信号统称为非可靠信号(早期是1~31这个范围)。目前Linux继承这类信号(1~31信号编号表示非可靠信号)。
可靠信号:在原来的基础上,增加了另外一批可靠信号,增加这批(34~64范围)信号支持队列(当处理一个信号的时候接受到另外一个信号,其他信号需要进行排队,等当前正在执行的信号处理完毕,依次处理其他的信号),不会丢失,因此,称为信号为可靠信号(从SIGRTMIN~SIGRTMAX之间的信号)。
=========================================================================
实时信号与非实时信号的来源
因为早期的不可靠信号每个信号都有固定的含义,不支持排队---非实时信号(1~31早期的信号还容易丢失)
后期增加可靠信号每个信号可以用户自定义,并且支持排队功能--实时信号(34~64后期的信号实时信号)
4、信号列表
可以使用kill -l函数,观察当前linux系统里面的信号列表:
每个信号的含义如下:
1) SIGHUP:当用户退出 Shell 时,由该 Shell 启动所有进程都退接收到这个信号,默认动作为终止进程。
2) SIGINT:用户按下组合 Ctrl 键+c 键时,用户端时向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
3) SIGQUIT:当用户按下组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信 号。默认动作为终止进程并产生 core 文件。
4) SIGILL :CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件。
5) SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止进程并产生 core 文件。
6) SIGABRT:调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。
7) SIGBUS:非法访问内存地址,包括内存地址对齐(alignment)出错,默认动作为终止进程并产生 core 文件。
8) SIGFPE:在发生致命的算术错误时产生。不仅包括浮点运行错误,还包括溢出及除数为 0 等所有的算 术错误。默认动作为终止进程并产生 core 文件。
9) SIGKILL:无条件终止进程。本信号不能被忽略、不可变更处理方式和阻塞。默认动作为终止进程。它 向系统管理员提供了一种可以杀死任何进程的方法。
10) SIGUSR1:用户定义的信号,即程序可以在程序中定义并使用该信号。默认动作为终止进程。
11) SIGSEGV:指示进程进行无效的内存访问。默认动作为终止进程并使用该信号。默认动作为终止进程。
12) SIGUSR2:这是另外一个用户定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进 程。 13) SIGPIPE:Broken pipe:向一个没有读端的管道写数据。默认动作为终止进程。
14) SIGALRM:定时器超时,超时的时间由系统调用 alarm 设置。默认动作为终止进程。
15) SIGTERM:程序结束(terminate)信号,与 SIGKILL 不同的是,该信号可以被阻塞和处理。通常用来要求 程序正常退出。执行 Shell 命令 kill 时,缺少产生这个信号。默认动作为终止进程。
17) SIGCHLD:子程序结束时,父进程会收到这个信号。默认动作为忽略该信号。
18) SIGCONT:让一个暂停的进程继续执行。
19) SIGSTOP:停止(stopped)进程的执行。注意它和 SIGTERM 以及 SIGINT 的区别:该进程还未结束, 只是暂停执行。本信号不能被忽略、处理和阻塞。默认作为暂停进程。
20) SIGTSTP:停止进程的动作,但该信号可以被处理和忽略。按下组合键时发出该信号。默认动作为暂停进程。
21) SIGTTIN:当后台进程要从用户终端读数据时,该终端中的所有进程会收到 SIGTTIN 信号。默认动作 为暂停进程。
22) SIGTTOU:该信号类似于 SIGTIN,在后台进程要向终端输出数据时产生。默认动作为暂停进程。(如 果不行需要设置 stty tostop)
23) SIGURG:套接字(socket)上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达。 默认动作为忽略该信号。
24) SIGXCPU:进程执行时间超过了分配给该进程的 CPU 时间,系统产生该信号并发送给该进程。默认动 作为终止进程。
25) SIGXFSZ:超过文件最大长度的限制。默认动作为 yl 终止进程并产生 core 文件。
26) SIGVTALRM:虚拟时钟超时时产生该信号。类似于 SIGALRM,但是它只计算该进程占有用的 CPU 时 间。默认动作为终止进程。
27) SIGPROF:类似于 SIGVTALRM,它不仅包括该进程占用的 CPU 时间还抱括执行系统调用的时间。默 认动作为终止进程。
28) SIGWINCH:窗口大小改变时发出。默认动作为忽略该信号。
29) SIGIO:此信号向进程指示发出一个异步 IO 事件。默认动作为忽略。
30) SIGPWR:关机。默认动作为终止进程。
31) SIGSYS:非法系统调用
34) SIGRTMIN~SIGRTMAX:Linux 的实时信号,它没有固定的含义(或者说可以由用户自由使用)—可以自己设定处理方式。注意, Linux 线程机制使用了前 3 个实时信号。所有的实时信号的默认动作都是终止进程。
注意:标准的写法应该是使用-前缀来明确指定信号编号,如kill -9 PID号
5、通信流程
一个完整的信号生命周期可以分为3个阶段:这3个阶段里面有4个重要的事情组成:信号产生(linux内核)、信号在进程中注册,信号的注销、执行信号的处理函数。
补充:一个用户进程不能直接给另外一个用户进程直接发送信号,必须通过内核来给另外一个用户进程发送信号。用户空间不具备发送信号的能力,进程把信号传递给内核中,所以必须通过内核来发送信号。
6、相关函数
发送信号
kill
函数功能:
给任一进程/进程组(除了当前进程),发送任一信号
头文件:
#include <sys/types.h>
#include <signal.h>
函数原型:
int kill(pid_t pid, int sig);
函数参数:
pid_t pid:对应进程的PID号
int sig:信号的编号—>可以使用kill -l查看
pid > 0 发送信号给进程的PID
pid = 0 发送信号当前进程组下的所有进程
pid = -1尽可能给进程组下所有的进程发送信号
pid <-1 发送信号给进程组-pid的进程发送相应的信号
函数返回值:
成功 0
失败 -1
示例1:使用一下kill命令,给当前进程发送一个9号信号
现象:
代码:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
//获取当前进程的PID
pid_t pid;
pid=getpid();
//输出一下当前进程的PID
printf("pid=%d\r\n",pid);
while(1);
return 0;
}
示例2:进程B使用一下kill函数,给进程A发送一个信号
现象:
现象:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
//获取进程A的PID号
int A = atoi(argv[1]);
//输出一下当前进程的PID
printf("A=%d\r\n",A);
//利用内核向进程A发送信号---杀死进程编号9
int ret = kill(A,9);
sleep(5);
if(ret == 0)
{
printf("kill success\r\n");
}
return 0;
}
alarm
函数功能:
可以定时对应的时间,时间到了可以发送闹钟信号给当前进程;如果设置秒数为0,取消对应的闹钟事件
头文件:
#include <unistd.h>
函数原型:
unsigned int alarm(unsigned int seconds);
函数参数:
unsigned int seconds:就是定时时间的长度
函数返回值:
返回上一次,闹钟事件的剩余时间
示例1:调用对应的alarm函数,观察对应的现象
现象:
代码:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
printf("hello\r\n");
//调用alarm函数
int ret = alarm(5);//无阻塞作用,默认为终止进程
printf("ret=%d\r\n",ret);
printf("world\r\n");
while(1);
return 0;
}
示例2:验证在多个闹钟的情况下,唯有最后一个闹钟才会生效
现象:
代码:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int ret;
printf("hello\r\n");
//调用alarm函数
ret= alarm(3);//无阻塞作用
printf("ret=%d\r\n",ret);
sleep(2);
//调用alarm函数
ret = alarm(5);//无阻塞作用
printf("ret=%d\r\n",ret);
//调用alarm函数
ret = alarm(7);//无阻塞作用
printf("ret=%d\r\n",ret);
printf("world\r\n");
while(1);
return 0;
}
raise
函数功能:
给当前线程(主进程)发送任一信号
头文件:
#include <signal.h>
函数原型:
int raise(int sig);
函数参数:
int sig:信号的编号—>可以使用kill -l查看
函数返回值:
成功 0
失败 -1
示例:调用一下raise函数,观察一下对应的现象
现象:
代码:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
printf("hello\r\n");
//调用raise函数
int ret = raise(2);//终止进程
printf("world\r\n");
return 0;
}
等待接收信号
pause
函数功能:
将当前的进程阻塞,直到接到信号
头文件:
#include <unistd.h>
函数原型:
int pause(void);
函数参数:无
函数返回值:
成功 -1
失败 错误
示例:调用一下pause函数,等待对应的信号到来。
现象:
代码:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int ret;
printf("hello\r\n");
//调用alarm函数
ret = alarm(2);//无阻塞作用
printf("ret=%d\r\n",ret);
printf("world\r\n");
//等待对应的信号到来
ret = pause();
printf("ret=%d\r\n",ret);
return 0;
}
处理信号
signal
函数功能:
设置当前进程对信号标号的处理方式
头文件:
#include <signal.h>
函数原型:
sighandler_t signal(int signum, sighandler_t handler);
函数参数:
int signum:信号的编号—kill -l
sighandler_t handler:表示信号的处理方式,处理方式主要分为三种:
第一个:SIG_DFL 对信号采取默认的处理方式
第二个:SIG_IGN 忽略该信号
第三个:信号处理函数的名称---注意函数的声明格式(自定义)
例子:void signal_handler(int arg);//信号处理函数
函数返回值:
成功 返回前一次处理信号的方式
失败 SIG_ERR
示例1:调用一下signal函数,观察一下现象
现象:
代码:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int ret;
printf("hello\r\n");
//设置一下,信号的处理方式
signal(14, SIG_DFL);//默认
//调用alarm函数
ret = alarm(2);//无阻塞作用
//等待对应的信号到来
ret = pause();
printf("world\r\n");
//等待对应的信号到来
ret = pause();
printf("ret=%d\r\n",ret);
return 0;
}
示例2:通过signal函数,验证arg是否是信号编号
现象:
代码:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
//信号处理函数
void signal_handler(int arg)
{
printf("arg:%d\r\n",arg);
if(arg == 14)
{
printf("hello\r\n");
}
}
int main(int argc,char *argv[])
{
//设置一下,信号的处理方式
signal(14,signal_handler);//默认
//调用alarm函数
alarm(5);//无阻塞作用
pause();
return 0;
}
综合实例
综合1:使用alarm定时器—0~5秒段—忽略 5~10秒 –自定义的功能 10之后恢复默认
现象:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void signal_handler(int arg)//信号处理函数
{
printf("hello_world\r\n");
}
int main(int argc,char *argv[])
{
//忽略
signal(14,SIG_IGN);
//定时
alarm(3);
int i = 1;
for(;i<6;i++)
{
printf("%dS\r\n",i);
sleep(1);
}
//自定义的功能
signal(14,signal_handler);
//定时
alarm(3);
for(i=6;i<11;i++)
{
printf("%dS\r\n",i);
sleep(1);
}
//默认
signal(14,SIG_DFL);
//定时
alarm(2);
for(i=11;i<15;i++)
{
printf("%dS\r\n",i);
sleep(1);
}
}
注意:alarm每次定时,到时间后,便会清零,不会累加;alarm无阻塞功能.
综合2:利用signal函数,观察一下信号处理函数的参数怎么变化的,利用多个信号
现象:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void signal_handler(int arg)//信号处理函数
{
if(arg == 14)
{
printf("arg=%d\r\n",arg);
printf("hello_world\r\n");
}
else if(arg == 1)
{
printf("arg=%d\r\n",arg);
printf("hello_whl\r\n");
}
else if(arg == 3)
{
printf("arg=%d\r\n",arg);
printf("hello_whl\r\n");
}
}
int main(int argc,char *argv[])
{
//获取当前进程的PID
pid_t pid;
pid=getpid();
printf("pid:%d\r\n",pid);
//自定义的功能-信号14
signal(14,signal_handler);
//定时
alarm(10);
//自定义的功能-信号1
signal(1,signal_handler);
//自定义的功能-信号3
signal(3,signal_handler);
int i = 1;
while(1)
{
printf("time:%ds\r\n",i++);
sleep(1);
}
}
注意:需要触发对应的信号后,才会执行信号处理函数
综合3:利用alarm()函数—定时器的功能,循环定时的功能—例如:每隔5秒获取一下时间
现象:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void signal_handler(int arg)//信号处理函数
{
if(arg == 14)
{
//获取系统时间
system("date");
//再次定时
alarm(5);
}
}
int main(int argc,char *argv[])
{
//自定义的功能-信号14
signal(14,signal_handler);
//定时
alarm(5);
int i = 1;
while(1)
{
printf("time:%ds\r\n",i++);
sleep(1);
}
}
综合4:改进一下上面的代码,利用2号信号,观察一下对应的现象—输出当前时间。
现象:
硬件方式触发:快捷键ctrl+c->信号2->获取时间
软件方式触发:kill -2 进程号
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void signal_handler(int arg)//信号处理函数
{
if(arg == 14)
{
//获取系统时间
system("date");
//再次定时
alarm(5);
}
else if(arg == 2)
{
//获取系统时间
system("date");
}
}
int main(int argc,char *argv[])
{
//获取当前进程的PID
pid_t pid;
pid=getpid();
printf("pid:%d\r\n",pid);
//自定义的功能-信号14
signal(14,signal_handler);
//定时
alarm(5);
//自定义的功能-信号2
signal(2,signal_handler);
int i = 1;
while(1)
{
printf("time:%ds\r\n",i++);
sleep(1);
}
}
三、共享内存
1、定义
共享内存(Shared Memory)允许两个或多个进程访问同一块内存区域,从而实现数据共享和交互。每个进程都有独立的虚拟地址空间和与之对应的页表,页表负责将进程的虚拟地址空间与物理地址空间进行映射,通过内存管理单元(MMU)进行管理。在共享内存通信中,不同的进程将相同的物理内存区域与各自的虚拟地址空间进行关联,从而实现通过虚拟地址直接访问共享内存。
2、特性
①共享内存是进程通信最快的通信方式,并且适用多进程大数据共享。
②共享内存没有保护机制—通常要加上约束,互斥方法
③共享内存的基本单位页,大小4K,并向上取整数
④共享内存的生命周期随内核—如果linux系统退出了,那么共享内存就会消失。
3、通信流程
1、创建.txt文件与一个项目id,生成唯一的标识符key--->ftok()
2、基于这个标识符(key))创建一个新的共享内存段或访问一个已存在的共享内存段,从而获得共享内存标识符shmid--->shmget()
3、通过获取到的id,完成共享内存在进程中的映射--->shmat()
4、利用映射地址对共享内存进行读写操作--->scanf()、gets()/puts()
5、取消共享内存在进程中的映射--->shmdt()
6、删除共享内存--->shmctl()
4、相关函数
ftok
函数功能:
获取key值
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
key_t ftok(const char *pathname, int proj_id);
函数参数:
const char *pathname:路径名---必须以字符串的形式存在,要求文件必须存在
int proj_id:工程ID->非0值
函数返回值:
成功 基于pathname(文件名1.txt) + proj_id,返回值相应的key值
失败 返回-1
示例:利用一下ftok函数,获取对应的key键值
现象:
代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc,char *argv[])
{
key_t key;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
printf("key:%d\r\n",key);
return 0;
}
shmget
函数功能:
申请打开一个基于key的共享内存从而获取ID。
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
int shmget(key_t key, size_t size, int shmflg);
函数参数:
key_t key:key值,可以通过ftok()返回值
size_t size:申请共享内存的大小->定义位4K大小--4096
int shmflg:打开共享内存的方式,一般情况IPC_CREAT | 0666
函数返回值:
成功 返回共享内存的id--shmid
失败 返回-1
注意: key值如果设置为0 ,也会申请一块共享内存,但是共享内存仅限本进程使用
示例:利用shmget函数,获取对应共享内存的ID号
现象:
代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
return 0;
}
shmat
函数功能:
通过shmget获取到的ID,完成共享内存在进程中的映射
头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数参数:
int shmid:共享内存的ID
const void *shmaddr:共享内存在进程中的映射地址,一般填写为NULL 表示系统自动分配
int shmflg:表示对该共享内存的操作方式,一般填写为0->表示可读可写
函数返回值:
成功 返回共享内存在进程中映射地址->可以定义一个p接收
失败 返回(void*)-1
示例1:利用一下shmat函数,可以获取对应的共享内存的首地址,往共享内容写入数据并且读取
现象:
代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
if(p != NULL)
{
printf("shmat success\r\n");
printf("p=%p\r\n",p);
}
//往对应的共享内存里面放入数据
fgets(p,10,stdin);
//读取共享内存内的数据
puts(p);
return 0;
}
示例2:实现两个不同的进程通过共享内存实现对数据的读写
现象:
代码:
进程A
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
if(p != NULL)
{
printf("shmat success\r\n");
printf("p=%p\r\n",p);
}
while(1)
{
//往对应的共享内存里面放入数据
fgets(p,10,stdin);
}
return 0;
}
进程B
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
if(p != NULL)
{
printf("shmat success\r\n");
printf("p=%p\r\n",p);
}
//往对应的共享内存里面放入数据
//fgets(p,10,stdin);
while(1)
{
//读取共享内存内的数据
puts(p);
sleep(5);
}
return 0;
}
shmdt
函数功能:
取消共享内存在本进程中的映射
头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:
int shmdt(const void *shmaddr);
函数参数:
const void *shmaddr:表示共享内存在进程中的映射地址,shmat函数的返回值
函数返回值:
成功 返回0
失败 返回-1
示例:调用一下shmdt函数,观察对应的现象
现象:
代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
if(p != NULL)
{
printf("shmat success\r\n");
printf("p=%p\r\n",p);
}
//往对应的共享内存里面放入数据
while(1)
{
//读取共享内存内的数据
puts(p);
sleep(5);
//取消共享内存在本进程中的映射
int ret = shmdt(p);
if(ret==0)
printf("shmdt success\r\n");
}
return 0;
}
shmctl
函数功能:
反应共享内存状态信息的
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数参数:
int shmid:表示的内存的ID
int cmd:表示共享内存的操作模式
IPC_STAT(查看状态) IPC_SET(设置) IPC_RMID(删除)
struct shmid_ds *buf:① IPC_RMID :buf里面填写为NULL ② IPC_STAT(查看状态) IPC_SET(设置) 见下方
函数返回值:
成功 返回0
失败 返回-1
IPC_STAT(查看状态) 与 IPC_SET(设置) 所用到的结构体:
struct shmid_ds {
struct ipc_perm shm_perm; 共享内存段的权限和所有权信息
size_t shm_segsz;内存大小
time_t shm_atime;最后一次映射到共享内存段的时间
time_t shm_dtime;表示最后一次取消映射共享内存段的时间
time_t shm_ctime;最后一次改变共享内存段的状态的时间
pid_t shm_cpid;创建共享内存段的进程的PID(进程ID)
pid_t shm_lpid;最后一次执行shmat或shmdt操作的进程的PID
shmatt_t shm_nattch;当前映射到共享内存段的进程数量
...
};
struct ipc_perm {
key_t __key; 用于创建或访问共享内存段的键
uid_t uid;共享内存段当前所有者的有效用户ID(UID)
gid_t gid;共享内存段当前所有者的有效组ID(GID)
uid_t cuid;创建共享内存段时的有效用户ID
gid_t cgid; 创建共享内存段时的有效组ID
unsigned short mode; 共享内存段的权限标志
unsigned short __seq; 序列号,用于内部跟踪和同步
};
示例1:利用一下shmctl函数,IPC_RMID观察对应的现象
现象:
代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
if(p != NULL)
{
printf("shmat success\r\n");
printf("p=%p\r\n",p);
}
//往共享内存写入数据
scanf("%s",p);
//读取共享内存内的数据
puts(p);
//删除共享内存空间
int ret = shmctl(id, IPC_RMID, NULL);
if(ret == 0)
printf("remove success\r\n");
return 0;
}
示例2:利用IPC_STAT,观察对应共享内存的状态信息
现象:
代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
if(p != NULL)
{
printf("shmat success\r\n");
printf("p=%p\r\n",p);
}
//往共享内存写入数据
scanf("%s",p);
//读取共享内存内的数据
puts(p);
//定义共享内存的状态信息结构体变量
struct shmid_ds shm_buf={0};
//观察对应共享内存的状态信息
ret=shmctl(shmid,IPC_STAT,&shm_buf);
if(ret == 0)
{
printf("shm_buf.shm_segsz:%d\r\n",shm_buf.shm_segsz);//查看内存大小
printf("shm_buf.shm_perm.mode:%d\r\n",shm_buf.shm_perm.mode);//查看修改权限
}
//删除共享内存空间
int ret = shmctl(id, IPC_RMID, NULL);
if(ret == 0)
printf("remove success\r\n");
return 0;
}
示例3:利用IPC_SET设置一下共享内存的权限—mode,观察
现象:
代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
int ret;
//获取键值
key = ftok("1.txt",1);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
if(p != NULL)
{
printf("shmat success\r\n");
printf("p=%p\r\n",p);
}
//往共享内存写入数据
scanf("%s",p);
//读取共享内存内的数据
puts(p);
//定义共享内存的状态信息结构体变量
struct shmid_ds shm_buf={0};
//观察对应共享内存的状态信息
ret=shmctl(id,IPC_STAT,&shm_buf);
if(ret == 0)
{
printf("shm_buf.shm_segsz:%d\r\n",shm_buf.shm_segsz);//查看内存大小
printf("shm_buf.shm_perm.mode:%d\r\n",shm_buf.shm_perm.mode);//查看修改权限
}
//设置权限
shm_buf.shm_perm.mode = 0664;
ret=shmctl(id,IPC_SET,&shm_buf);
if(ret==0)
{
printf("set success\r\n");
}
//再次观察对应共享内存的状态信息
ret=shmctl(id,IPC_STAT,&shm_buf);
if(ret == 0)
{
printf("shm_buf.shm_segsz:%d\r\n",shm_buf.shm_segsz);//查看内存大小
printf("shm_buf.shm_perm.mode:%d\r\n",shm_buf.shm_perm.mode);//查修改权限
}
//删除共享内存空间
ret = shmctl(id, IPC_RMID, NULL);
if(ret == 0)
printf("remove success\r\n");
return 0;
}
综合实例
示例:多进程利用共享内存进行自由通信
现象:
代码:
进程A、进程B
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
key_t key;
int id;
char *p;
int ret;
/*创建共享内存*/
//获取键值
key = ftok("./2.txt",0);
printf("key:%x\r\n",key);
//获取对应共享内存的ID号
id = shmget(key,4096, IPC_CREAT | 0666);
printf("id:%d\r\n",id);
//将共享内存映射到,当前这个进程里面
p = (char*)shmat(id, NULL, 0);
/*创建父子进程*/
ret = fork();
if(ret == 0)//子进程
{
char buff[10]={0};
while(1)
{
if(strcmp(buff,p)!=0)
{
//读取共享内存内的数据
puts(p);
//将数据写入数组内
strcpy(buff,p);
printf("buff:%s\r\n",buff);
if(strcmp(buff,"quit")==0)
{
break;
}
}
else
{
sleep(1);
}
}
//退出进程
exit(0);
}
else//父进程
{
while(1)
{
//往共享内存写入数据
scanf("%s",p);
//读取数据
puts(p);
if(strcmp("quit",p)==0)
{
break;
}
}
}
//删除共享内存
ret=shmctl(id,IPC_RMID,NULL);
printf("ret=%d\r\n",ret);
//等待对应的子进程
wait(NULL);
return 0;
}
需要解决的问题:如何避免重复获取相同的数据?---》类似于裸机的第一次开机
四、消息队列
1、特性
消息队列的特点:
①采用链式结构
②消息队列上传消息之后,除非消息被接收或消息被删除,否则消息不会消失 (并且只能被正确接收一次)
③消息队列具阻塞特性:当缓冲区满时,写操作会阻塞,直到有空间可用;当缓冲区为空时,读操作会阻塞(消息队列读阻塞,消息队列里面没有内容, 当前的这个进程会被阻塞掉),直到有数据可读
=========================================================================
消息队列与FIFO结构(先进先出),都有一个队列结构,并可以实现多进程往队列中写入信息,以及多进程往消息队列里面读取消息。FIFO结构正常有两个端口,事先先打开,这样才能正常传递消息。
2、通信流程
①创建.txt文件与一个项目id,生成唯一的标识符key--->ftok()
②基于这个标识符(key)创建一个新的消息队列或访问一个已存在的消息队列,从而获得消息队列标识符msgid--->msgget()
③利用消息队列标识符msgid以及消息编号与消息数据,往指定的消息队列上发送信息或者读取信息--->msgsnd()、msgrcv()
④删除消息列表--->msgctl()
3、相关函数
ftok
函数功能:
获取key值
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
key_t ftok(const char *pathname, int proj_id);
函数参数:
const char *pathname:路径名---必须以字符串的形式存在,要求文件必须存在
int proj_id:工程ID->非0值
函数返回值:
成功 基于pathname(文件名1.txt) + proj_id,返回值相应的key值
失败 返回-1
msgget
函数功能:
创建一个新的消息队列或者获取一个已经存在的消息队列的标识符.
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:
int msgget(key_t key, int msgflg);
函数参数:
key_t key:key值,可以通过ftok()返回值
int msgflg:用于控制消息队列的创建和访问权限->IPC_CREAT|0664
函数返回值:
成功 返回消息队列的ID
失败 返回-1
示例:利用msgget函数,获取对应的消息ID
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc,char* argv[])
{
key_t key;
int id;
//获取键值
key = ftok("3.txt",4);
printf("key:%x\r\n",key);
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
}
msgsnd
函数功能:
往指定的消息队列上发送信息
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:
int msgsnd(mqd_t msqid, const void *msg_ptr, size_t msg_len, int msgflg);
函数参数:
int msqid:消息队列描述符,是通过 msgget() 函数获得的消息队列标识符。
const void *msg_ptr:发送信息的指针。这个消息是一个结构体,包括消息编号和需要发送消息的数据
size_t msg_len:消息数据的长度(不包括消息类型字段)
int msgflg:控制发送操作的标志。一般设置为 0(阻塞),设置为 IPC_NOWAIT(非阻塞)
函数返回值:
成功 返回0
失败 返回-1
补充:
一般先确定消息的类型,定义并赋值,最后再发送消息
msgsnd函数,它的第二个参数,要指向结构体类型的:
struct msgbuf
{
long mtype; --->消息的类型---指的就是消息编号 1
char mtext[]; ---->消息的数据
}
示例:调用msgsnd,将数据发送出去,观察现象
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
struct msgbuf
{
long mtype; //--->消息的类型---指的就是消息编号 1
char mtext[20]; //---->消息的数据
};
int main(int argc,char* argv[])
{
key_t key;
int msgid,ret;
struct msgbuf buff_w={2,"hello_world"};
//获取键值
key = ftok("3.txt",4);
printf("key:%x\r\n",key);
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
//发送数据信息
ret = msgsnd(msgid,&buff_w,sizeof(buff_w), 0);
if(ret == 0)
printf("send success\r\n");
return 0;
}
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);
函数参数:
int msqid:消息队列描述符,是通过 msgget() 函数获得的消息队列标识符。
void *msgp:接收信息的指针
size_t msgsz:指向一个用户定义的缓冲区,该缓冲区用于存储接收到的消息。这个缓冲区需要足够大,以容纳消息结构(包括消息类型和消息文本)
long msgtyp:表示接收信息的编号
int msgflg:控制接收操作的标志。设置为 0(阻塞等待消息),设置为 IPC_NOWAIT(非阻塞,如果没有消息立即返回-1并设置errno)
注意:①在阻塞模式下,msgrcv()只有在成功接收到消息或发生错误时才会返回;在阻塞等待期间, 它不会返回任何值 ②在非阻塞模式下,msgrcv()将不会阻塞调用线程以等待消息的到来;相反,它会立即返回,并根据消息队列中是否有符合条件的消息来决定返回值和后续操作。
函数返回值:
成功 返回接收信息数据的长度(不包括信息类型)
失败 返回-1
示例1:利用一下msgrcv()函数,接收消息队列上的消息
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
struct msgbuf
{
long mtype; //--->消息的类型---指的就是消息编号 1
char mtext[20]; //---->消息的数据
};
int main(int argc,char* argv[])
{
key_t key;
int msgid,ret;
struct msgbuf buff_r;
//获取键值
key = ftok("3.txt",4);
printf("key:%x\r\n",key);
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
//接收数据信息
ret = msgrcv(msgid, &buff_r, sizeof(buff_r), 2,0);
printf("ret:%d\r\n",ret);
printf("buff_r:%s\r\n",buff_r.mtext);
printf("mtype:%ld\r\n",buff_r.mtype);
return 0;
}
示例2:利用父子进程,实现父进程操作消息队列,发送信息给子进程
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
struct msgbuf
{
long mtype; //--->消息的类型---指的就是消息编号 1
char mtext[20]; //---->消息的数据
};
int main(int argc,char* argv[])
{
key_t key;
int msgid,ret;
struct msgbuf buff_w={3,0};
//获取键值
key = ftok("1.txt",4);
printf("key:%x\r\n",key);
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
//创建父子进程
ret = fork();
if(ret == 0)//子进程
{
struct msgbuf buff_r;
//接收数据信息
msgrcv(msgid, &buff_r, sizeof(buff_r), 3,0);
printf("buff_r:%s\r\n",buff_r.mtext);
printf("mtype:%ld\r\n",buff_r.mtype);
}
else//父进程
{
scanf("%s",buff_w.mtext);
//发送数据信息
ret = msgsnd(msgid,&buff_w,sizeof(buff_w), 0);
if(ret == 0)
printf("send success\r\n");
}
}
msgctl
函数功能:
反应消息队列状态信息的
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
函数参数:
int msqid:消息队列描述符,是通过 msgget() 函数获得的消息队列标识符。
int cmd:表示消息队列的操作模式
IPC_STAT(查看状态) IPC_SET(设置) IPC_RMID(删除)
struct msqid_ds *buf: IPC_RMID :buf里面填写为NULL
IPC_SET:buf用来设置消息队列
函数返回值:
成功 返回0
失败 返回-1
示例:可以将对应的消息队列,删除掉。
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
struct msgbuf
{
long mtype; //--->消息的类型---指的就是消息编号 1
char mtext[20]; //---->消息的数据
};
int main(int argc,char* argv[])
{
key_t key;
int msgid,ret;
struct msgbuf buff_w={3,0};
//获取键值
key = ftok("1.txt",4);
printf("key:%x\r\n",key);
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
//创建父子进程
ret = fork();
if(ret == 0)//子进程
{
struct msgbuf buff_r;
//接收数据信息
msgrcv(msgid, &buff_r, sizeof(buff_r), 3,0);
printf("buff_r:%s\r\n",buff_r.mtext);
printf("mtype:%ld\r\n",buff_r.mtype);
//退出子进程
exit(0);
}
else//父进程
{
scanf("%s",buff_w.mtext);
//发送数据信息
ret = msgsnd(msgid,&buff_w,sizeof(buff_w), 0);
if(ret == 0)
printf("send success\r\n");
}
//等待子进程执行完毕
wait(NULL);
//删除消息队列
ret = msgctl(msgid, IPC_RMID,NULL);
if(ret == 0)
printf("remove success\r\n");
return 0;
}
综合实例
综合:利用进程A、进程B和进程C,操作消息队列,实现自由通信。
思路: 1: 进程A给对应的进程B发送信息的时候,信号的编号是1
2: 进程A给对应的进程C发送信息的时候,信号的编号是2
3: 进程B给对应的进程A发送信息的时候,信号的编号是3
4: 进程B给对应的进程C发送信息的时候,信号的编号是4
5: 进程C给对应的进程A发送信息的时候,信号的编号是5
6: 进程C给对应的进程B发送信息的时候,信号的编号是6
现象:
进程A发数据给进程B和进程C
进程B发数据给进程A和进程C
进程C发数据给进程A和进程B
代码:
进程A
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
struct msgbuf
{
long mtype; //--->消息的类型---指的就是消息编号 1
char mtext[20]; //---->消息的数据
};
int main(int argc,char* argv[])
{
key_t key;
int ret;
//获取键值
key = ftok("3.txt",4);
printf("key:%x\r\n",key);
int msgid;
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
//创建父子进程
ret = fork();
if(ret == 0)//子进程
{
struct msgbuf buff_r;
while(1)
{
//接收数据信息
ret = msgrcv(msgid, &buff_r, sizeof(buff_r),3,IPC_NOWAIT);//非阻塞,如果没有消息立即返回-1
if(ret == -1)
{
;
}
else
{
printf("ret:%d\r\n",ret);
printf("B_to_A:%s\r\n",buff_r.mtext+3);
printf("mtype:%ld\r\n",buff_r.mtype);
}
//接收数据信息
ret = msgrcv(msgid, &buff_r, sizeof(buff_r),5,IPC_NOWAIT);//非阻塞,如果没有消息立即返回-1
if(ret == -1)
{
;
}
else
{
printf("ret:%d\r\n",ret);
printf("C_to_A:%s\r\n",buff_r.mtext+3);
printf("mtype:%ld\r\n",buff_r.mtype);
}
//清空数组
memset(&buff_r,0,sizeof(buff_r));
}
//退出子进程
exit(0);
}
else//父进程
{
struct msgbuf buff_1={0};
while(1)
{
scanf("%[^\n]",buff_1.mtext);//可以忽略空格
getchar();
if(strncmp(buff_1.mtext,"toB",3)==0)
{
//给对应变量赋值
buff_1.mtype = 1;
//发送数据信息
ret = msgsnd(msgid,&buff_1,sizeof(buff_1),0);
if(ret == 0)
printf("buff1 send success\r\n");
}
else if(strncmp(buff_1.mtext,"toC",3)==0)
{
//给对应变量赋值
buff_1.mtype = 2;
//发送数据信息
ret = msgsnd(msgid,&buff_1,sizeof(buff_1), 0);
if(ret == 0)
printf("buff1 send success\r\n");
}
else if(strcmp("quit",buff_1.mtext)==0)
{
break;
}
else
{
printf("input toB or toC in the beginning\r\n");
}
//清空数组
memset(&buff_1.mtext,0,sizeof(buff_1.mtext));
}
}
//等待子进程执行完毕
wait(NULL);
//删除消息队列
ret = msgctl(msgid, IPC_RMID,NULL);
if(ret == 0)
printf("remove success\r\n");
return 0;
}
进程B
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
struct msgbuf
{
long mtype; //--->消息的类型---指的就是消息编号 1
char mtext[20]; //---->消息的数据
};
int main(int argc,char* argv[])
{
key_t key;
int ret;
//获取键值
key = ftok("3.txt",4);
printf("key:%x\r\n",key);
int msgid;
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
//创建父子进程
ret = fork();
if(ret == 0)//子进程
{
struct msgbuf buff_r;
while(1)
{
//接收数据信息
ret = msgrcv(msgid, &buff_r, sizeof(buff_r),1,IPC_NOWAIT);//非阻塞,如果没有消息立即返回-1
if(ret == -1)
{
;
}
else
{
printf("ret:%d\r\n",ret);
printf("A_to_B:%s\r\n",buff_r.mtext+3);
printf("mtype:%ld\r\n",buff_r.mtype);
}
//接收数据信息
ret = msgrcv(msgid, &buff_r, sizeof(buff_r),6,IPC_NOWAIT);//非阻塞,如果没有消息立即返回-1
if(ret == -1)
{
;
}
else
{
printf("ret:%d\r\n",ret);
printf("C_to_B:%s\r\n",buff_r.mtext+3);
printf("mtype:%ld\r\n",buff_r.mtype);
}
//清空数组
memset(&buff_r,0,sizeof(buff_r));
}
//退出子进程
exit(0);
}
else//父进程
{
struct msgbuf buff_2={0};
while(1)
{
scanf("%[^\n]",buff_2.mtext);//可以忽略空格
getchar();
if(strncmp(buff_2.mtext,"toA",3)==0)
{
//给对应变量赋值
buff_2.mtype = 3;
//发送数据信息
ret = msgsnd(msgid,&buff_2,sizeof(buff_2),0);
if(ret == 0)
printf("buff2 send success\r\n");
}
else if(strncmp(buff_2.mtext,"toC",3)==0)
{
//给对应变量赋值
buff_2.mtype = 4;
//发送数据信息
ret = msgsnd(msgid,&buff_2,sizeof(buff_2), 0);
if(ret == 0)
printf("buff2 send success\r\n");
}
else if(strcmp("quit",buff_2.mtext)==0)
{
break;
}
else
{
printf("input toA or toC in the beginning\r\n");
}
//清空数组
memset(&buff_2.mtext,0,sizeof(buff_2.mtext));
}
}
//等待子进程执行完毕
wait(NULL);
//删除消息队列
ret = msgctl(msgid, IPC_RMID,NULL);
if(ret == 0)
printf("remove success\r\n");
return 0;
}
进程C
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
struct msgbuf
{
long mtype; //--->消息的类型---指的就是消息编号 1
char mtext[20]; //---->消息的数据
};
int main(int argc,char* argv[])
{
key_t key;
int ret;
//获取键值
key = ftok("3.txt",4);
printf("key:%x\r\n",key);
int msgid;
//获取对应消息队列的ID号
msgid = msgget(key,IPC_CREAT|0664);
printf("msgid:%d\r\n",msgid);
//创建父子进程
ret = fork();
if(ret == 0)//子进程
{
struct msgbuf buff_r;
while(1)
{
//接收数据信息
ret = msgrcv(msgid, &buff_r, sizeof(buff_r),2,IPC_NOWAIT);//非阻塞,如果没有消息立即返回-1
if(ret == -1)
{
;
}
else
{
printf("ret:%d\r\n",ret);
printf("A_to_C:%s\r\n",buff_r.mtext+3);
printf("mtype:%ld\r\n",buff_r.mtype);
}
//接收数据信息
ret = msgrcv(msgid, &buff_r, sizeof(buff_r),4,IPC_NOWAIT);//非阻塞,如果没有消息立即返回-1
if(ret == -1)
{
;
}
else
{
printf("ret:%d\r\n",ret);
printf("B_to_C:%s\r\n",buff_r.mtext+3);
printf("mtype:%ld\r\n",buff_r.mtype);
}
//清空数组
memset(&buff_r,0,sizeof(buff_r));
}
//退出子进程
exit(0);
}
else//父进程
{
struct msgbuf buff_3={0};
while(1)
{
scanf("%[^\n]",buff_3.mtext);//可以忽略空格
getchar();
if(strncmp(buff_3.mtext,"toA",3)==0)
{
//给对应变量赋值
buff_3.mtype = 5;
//发送数据信息
ret = msgsnd(msgid,&buff_3,sizeof(buff_3),0);
if(ret == 0)
printf("buff3 send success\r\n");
}
else if(strncmp(buff_3.mtext,"toB",3)==0)
{
//给对应变量赋值
buff_3.mtype = 6;
//发送数据信息
ret = msgsnd(msgid,&buff_3,sizeof(buff_3), 0);
if(ret == 0)
printf("buff3 send success\r\n");
}
else if(strcmp("quit",buff_3.mtext)==0)
{
break;
}
else
{
printf("input toA or toB in the beginning\r\n");
}
//清空数组
memset(&buff_3.mtext,0,sizeof(buff_3.mtext));
}
}
//等待子进程执行完毕
wait(NULL);
//删除消息队列
ret = msgctl(msgid, IPC_RMID,NULL);
if(ret == 0)
printf("remove success\r\n");
return 0;
}
五、信号量
1、定义
信号量(semaphore)是一种实现任务间通信的机制,可以实现任务之间同步(约束)或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。
本质:信号量值的改变是通过函数来实现的加减,阻塞原因是函数。
2、PV操作
操作信号量的方式,只有两种:申请信号量 和 释放信号量
P(等待/减)操作:当一个进程或线程需要访问共享资源时,它会尝试执行 P 操作。如果信号量的值大于 0,则进程可以继续访问资源,并将信号量的值减 1;如果信号量的值等于 0,则进程会被阻塞,直到信号量的值变为正数。
V(释放/加)操作:当一个进程或线程完成对共享资源的访问时,它会执行 V 操作,将信号量的值加 1。如果有其他等待进程被阻塞,它们中的一个将被唤醒并获得对资源的访问权限。
3、通信流程
①创建.txt文件与一个项目id,生成唯一的标识符key--->ftok()
②基于这个标识符(key)创建一个新的信号量集或访问一个已存在的信号量集,从而获得信号量集标识符semid--->semget()
③利用信号量集标识符semid与命令来控制对应的信号量--->semctl()
④利用信号量集标识符semid对信号量的值进行P(等待)和V(信号)操作--->semop()
4、相关函数
semget
函数功能:
用于创建并且打开信号量集,信号量的值默认值为0
头文件:
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semget(key_t key, int nsems, int semflg);
函数参数:
key_t key:这是一个用于标识信号量集的键值。多个进程可以通过相同的键值来访问同一个信号量集。通常,这个键是通过 ftok() 函数生成的。
int nsems:这个参数指定了信号量集中信号量的数量。信号量集中的每个信号量都可以独立地被操作和等待。
int semflg:这个参数是一个标志位,用于控制信号量集的创建和访问权限。它可以是 0(表示访问已存在的信号量集)、IPC_CREAT(表示如果信号量集不存在则创建它)、IPC_EXCL(与 IPC_CREAT 一起使用,表示如果信号量集已存在则调用失败)的组合,以及权限标志(如 0666,表示赋予所有用户读写权限)。
函数返回值:
成功 返回信号量集id
失败 返回-1
使用:
semget(key,1,IPC_CREAT | 0666);
semget(key,1,IPC_EXCL | 0666);
示例:利用semget函数,观察一下对应的现象
现象:
代码:
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/sem.h>
int main(int argc,char* argv[])
{
key_t key,semid;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,1,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
return 0;
}
semctl
函数功能:
用来操作控制对应的信号量
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semctl(int semid, int semnum, int cmd, union semun arg);
函数参数:
int semid:信号量集的标识符,由semget()函数返回
int semnum:信号量集中的信号量编号,从0开始
int cmd:指定要执行的命令,它决定了后面的参数和semctl()的行为.
cmd命令的一些常见值包括:
IPC_STAT:获得信号量的semid_ds数据结构,并存放在由第4个参数arg中buf成员中,semid_ds是系统中描述信号量的数据结构
IPC_RMID:从系统中删除信号量。
IPC_GETVAL或GETVAL: 获取对应编号的信号量的值
IPC_SETVAL或SETVAL:设置对应编号的信号量的值
GETALL:获取所有信号量的值 (获取的时候第二个参数为0)
SETALL:设置所有信号量的值(获取的时候第二个参数为0)
union semun arg:根据cmd的不同,这个参数可以是不同的类型。
函数返回值:
GETVAL:成功返回对应信号量的值
其他的cmd: 成功 返回0
错误 返回-1
注意:信号量集里面信号量的值默认都是0;IPC_RMID、GETVAL不需要用到第四个参数。
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
struct semid_ds {
struct ipc_perm sem_perm; 包含了信号量集的权限和所有权信息
time_t sem_otime; 最后一次对信号量集进行semop()操作的时间戳
time_t sem_ctime; 最后一次改变信号量集(通过semctl()的IPC_SET命令等)的时间戳
unsigned long sem_nsems;信号量集中信号量的数量
};
struct ipc_perm {
key_t key; 创建IPC对象时使用的键值
uid_t uid; IPC对象当前所有者的有效用户ID
gid_t gid; IPC对象当前所有者的组ID
uid_t cuid;创建IPC对象时的有效用户ID
gid_t cgid;创建IPC对象时的组ID
unsigned short mode;IPC对象的权限模式,类似于文件系统的权限模式
unsigned short __seq;序列号
};
说明:
如果我们要用到第四个参数,第四个参数传参的的时候,给共用体变量名即可,然后你要设置信号量集中哪个信号量的值的话,在共用体变量中赋值好,然后再调用semctl函数进行设置即可。
如我要设置信号量集中的第1个元素(编号为0)为4的话:
union semun sembuf;
memset(&sembuf, 0, sizeof(sembuf));
sembuf.val = 4;
int ret = semctl(semid, 0, SETVAL, sembuf);
示例1:利用semctl函数删除对应的信号量集
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/sem.h>
int main(int argc,char* argv[])
{
key_t key,semid;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,1,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
//利用semctl函数删除信号量集
int ret = semctl(semid,0,IPC_RMID);
if(ret == 0)
printf("RMID success\r\n");
return 0;
}
示例2:利用semctl函数,先设置信号量的值,再获取信号量的当前值
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/sem.h>
//声明共用体
union semun{
int val; /*Value for SETVAL*/
unsigned short *array; /*Array for GETALL, SETALL*/
};
int main(int argc,char* argv[])
{
key_t key,semid;
int ret,num;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,5,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
//定义共用体
union semun semval;
semval.val = 66;
//利用semctl函数,先设置信号量的值
ret = semctl(semid,2,SETVAL,semval);
if(ret==0)
printf("SET success\r\n");
//再利用semctl函数获取信号量的当前值
num = semctl(semid,2,GETVAL);
printf("num:%d\r\n",num);
//利用semctl函数删除信号量集
int ret = semctl(semid,0,IPC_RMID);
if(ret == 0)
printf("RMID success\r\n");
return 0;
}
示例3:设置所有信号量的值,再获取所有信号量的值,观察所有信号量的值
现象:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/sem.h>
//声明共用体
union semun{
int val; /*Value for SETVAL*/
unsigned short *array; /*Array for GETALL, SETALL*/
};
int main(int argc,char* argv[])
{
key_t key,semid;
int ret,num;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,5,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
//定义共用体
union semun semall;
//定义设置数组
unsigned short buff_w[]={1,2,3,4,5};
//将需要设置的数值传入到共用体
semall.array = buff_w;
//利用semctl函数,设置所有信号量的值
ret = semctl(semid,0,SETALL,semall);
if(ret==0)
printf("SETALL success\r\n");
//定义获取数组
unsigned short buff_r[10]={0};
//将需要获取到的数值传入到数组
semall.array = buff_r;
//利用semctl函数,再获取所有信号量的值
ret = semctl(semid,0,GETALL,semall);
if(ret==0)
{
printf("GETALL success\r\n");
for(int i =0;i<10;i++)
{
printf("buff_r[%d]=%d\r\n",i,buff_r[i]);
}
}
//利用semctl函数删除对应的信号量集
ret = semctl(semid,0,IPC_RMID);
if(ret == 0)
printf("RMID success\r\n");
return 0;
}
示例4:利用semctl函数,调用IPC_STAT命令,观察信号量的状态信息。
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/sem.h>
//声明共用体
union semun{
int val; /*Value for SETVAL*/
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /*Array for GETALL, SETALL*/
};
int main(int argc,char* argv[])
{
key_t key,semid;
int ret,num;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,5,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
//定义共用体
union semun semall;
//定义设置数组
unsigned short buff_w[]={1,2,3,4,5};
//将需要设置的数值传入到共用体
semall.array = buff_w;
//利用semctl函数,设置所有信号量的值
ret = semctl(semid,0,SETALL,semall);
if(ret==0)
printf("SETALL success\r\n");
//定义获取数组
unsigned short buff_r[10]={0};
//将需要获取到的数值传入到数组
semall.array = buff_r;
//利用semctl函数,再获取所有信号量的值
ret = semctl(semid,0,GETALL,semall);
if(ret==0)
{
printf("GETALL success\r\n");
for(int i =0;i<10;i++)
{
printf("buff_r[%d]=%d\r\n",i,buff_r[i]);
}
}
//定义结构体变量
struct semid_ds semsta = {0};
//将需要获取到的数值传入到结构体内
semall.buf = &semsta;
//调用IPC_STAT命令,观察信号量的状态信息
ret = semctl(semid,0,IPC_STAT,semall);
if(ret == 0)
{
printf("STAT success\r\n");
printf("semsta.sem_perm.key:%x\r\n",semsta.sem_perm. __key);//创建IPC对象时使用的键值
printf("semsta.sem_perm.mode:%d\r\n",semsta.sem_perm.mode);//IPC对象的权限模式
printf("semsta.sem_nsems:%ld\r\n",semsta.sem_nsems);//信号量集中信号量的数量
}
//利用semctl函数删除对应的信号量集
ret = semctl(semid,0,IPC_RMID);
if(ret == 0)
printf("RMID success\r\n");
return 0;
}
示例5:利用semctl函数,调用IPC_SET命令修改权限,再观察信号量的状态信息。
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/sem.h>
//声明共用体
union semun{
int val; /*Value for SETVAL*/
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /*Array for GETALL, SETALL*/
};
int main(int argc,char* argv[])
{
key_t key,semid;
int ret,num;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,5,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
//定义共用体
union semun semall;
//定义设置数组
unsigned short buff_w[]={1,2,3,4,5};
//将需要设置的数值传入到共用体
semall.array = buff_w;
//利用semctl函数,设置所有信号量的值
ret = semctl(semid,0,SETALL,semall);
if(ret==0)
printf("SETALL success\r\n");
//定义获取数组
unsigned short buff_r[10]={0};
//将需要获取到的数值传入到数组
semall.array = buff_r;
//利用semctl函数,再获取所有信号量的值
ret = semctl(semid,0,GETALL,semall);
if(ret==0)
{
printf("GETALL success\r\n");
for(int i =0;i<10;i++)
{
printf("buff_r[%d]=%d\r\n",i,buff_r[i]);
}
}
/*查看状态*/
//定义结构体变量
struct semid_ds semsta = {0};
//将需要获取到的数值传入到结构体内
semall.buf = &semsta;
//调用IPC_STAT命令,观察信号量的状态信息
ret = semctl(semid,0,IPC_STAT,semall);
if(ret == 0)
{
printf("STAREAD success\r\n");
printf("semsta.sem_perm.key:%x\r\n",semsta.sem_perm. __key);//创建IPC对象时使用的键值
printf("semsta.sem_perm.mode:%d\r\n",semsta.sem_perm.mode);//IPC对象的权限模式
printf("semsta.sem_nsems:%ld\r\n",semsta.sem_nsems);//信号量集中信号量的数量
}
/*设置状态*/
//定义结构体变量
struct semid_ds setsta = {0};
setsta.sem_perm.mode = 0664;//修改权限
//将需要设置的数值传入到共用体
semall.buf = &setsta;
//调用IPC_SET命令,观察信号量的状态信息
ret = semctl(semid,0,IPC_SET,semall);
if(ret == 0)
{
printf("STASET success\r\n");
}
/*查看状态*/
//将需要获取到的数值传入到结构体内
semall.buf = &semsta;
//调用IPC_STAT命令,观察信号量的状态信息
ret = semctl(semid,0,IPC_STAT,semall);
if(ret == 0)
{
printf("STAREAD success\r\n");
printf("semsta.sem_perm.key:%x\r\n",semsta.sem_perm. __key);//创建IPC对象时使用的键值
printf("semsta.sem_perm.mode:%d\r\n",semsta.sem_perm.mode);//IPC对象的权限模式
printf("semsta.sem_nsems:%ld\r\n",semsta.sem_nsems);//信号量集中信号量的数量
}
//利用semctl函数删除对应的信号量集
ret = semctl(semid,0,IPC_RMID);
if(ret == 0)
printf("RMID success\r\n");
return 0;
}
semop
函数功能:
对信号量的值进行加减操作
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semop(int semid, struct sembuf *sops, unsigned nsops);
函数参数:
int semid:信号量集的标识符,由semget()函数返回
struct sembuf *sops :指向 sembuf 结构数组的指针,每个 sembuf 结构定义了对信 号量集中的一个信号量的操作。
struct sembuf
{
unsigned short sem_num; /* 信号量在信号量集中的编号*/
short sem_op; /* 信号量变化值 PV操作正数代表+,负数代表-*/
short sem_flg; /* 一组标志 直接给0 */
}
说明:
sem_flg:信号量操作的属性标志
(1)如果为0,表示正常操作,如果信号量值为0,减操作就会阻塞
(2)如果为IPC_NOWAIT,表示对信号量的操作时非阻塞的。在信号量的值不满足条件的 情况下不会被阻塞,而是直接返回-1
(3)如果为SEM_UNDO,那么将维护进程对信号量的调整值,以便进程结束时恢复信号量 的状态。--- 暂时先不用
semop() 函数的工作方式如下:
根据 sops 数组中每个 sembuf 结构指定的信号量索引 (sem_num)、操作值 (sem_op) 和操作标志 (sem_flg),对信号量集中的信号量进行相应的操作。
如果 sem_op 为正数,表示释放相应数量的资源,信号量值增加。
如果 sem_op 为负数,表示请求相应数量的资源,如果信号量值足够,则减少信号量值并继续执行;如果信号量值不足,则根据 sem_flg 的设置,可能会阻塞等待资源释放。
如果 sem_op 为 0,通常用于测试信号量值,不会改变信号量值。
unsigned nsops :sops 数组中 sembuf 结构的数量,即要进行的操作数量
说明:
sizeof(结构体数组名) / sizeof(结构体数据类型名)即结构体数组中的元素数量
struct sembuff a[10]; 10 = sizeof(a)/sizeof(a[0])
struct sembuff a[10]表示定义了一个名为 a 的数组,该数组包含10个元素,每个元素都是 struct sembuff 类型
函数返回值:
成功 返回0
失败 返回-1
示例1:调用一下semop,操作一个信号量,第一个信号量进行V操作,观察对应的现象。
现象:
代码:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/sem.h>
//声明共用体
union semun{
int val; /*Value for SETVAL*/
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /*Array for GETALL, SETALL*/
};
int main(int argc,char* argv[])
{
key_t key,semid;
int ret,num;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,5,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
//定义共用体
union semun semall;
//定义设置数组
unsigned short buff_w[]={1,2,3,4,5};
//将需要设置的数值传入到共用体
semall.array = buff_w;
//利用semctl函数,设置所有信号量的值
ret = semctl(semid,0,SETALL,semall);
if(ret==0)
printf("SETALL success\r\n");
//定义获取数组
unsigned short buff_r[5]={0};
//将需要获取到的数值传入到数组
semall.array = buff_r;
//利用semctl函数,再获取所有信号量的值
ret = semctl(semid,0,GETALL,semall);
if(ret==0)
{
printf("GETALL success\r\n");
for(int i =0;i<5;i++)
{
printf("buff_r[%d]=%d\r\n",i,buff_r[i]);
}
}
//定义结构体变量-第一个信号量进行V操作
struct sembuf op[1] = {0,+10,0};
//调用semop,对信号量的值进行加减操作
ret = semop(semid, op, sizeof(op)/sizeof(op[0]));
if(ret == 0)
{
printf("OP success\r\n");
}
//定义获取数组
unsigned short buff[5]={0};
//将需要获取到的数值传入到数组
semall.array = buff;
//利用semctl函数,再获取所有信号量的值
ret = semctl(semid,0,GETALL,semall);
if(ret == 0)
{
//查看信号量的值是否进行加减操作
for(int i =0;i<5;i++)
{
printf("buff[%d]=%d\r\n",i,buff[i]);
}
}
//利用semctl函数删除对应的信号量集
ret = semctl(semid,0,IPC_RMID);
if(ret == 0)
printf("RMID success\r\n");
return 0;
}
示例2:利用semop函数,操作所有的信号量的值,获取所有信号量的值,观察一下
现象:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/sem.h>
//声明共用体
union semun{
int val; /*Value for SETVAL*/
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /*Array for GETALL, SETALL*/
};
int main(int argc,char* argv[])
{
key_t key,semid;
int ret,num;
//获取键值
key = ftok("./5.txt",1);
printf("key=%d\r\n",key);
//创建并且打开信号量集
semid = semget(key,5,IPC_CREAT | 0666);
printf("semid=%d\r\n",semid);
//定义共用体
union semun semall;
/*设置所有信号量的值--命令:SETALL*/
//定义设置数组
unsigned short buff_w[]={1,2,3,4,5};
//将需要设置的数值传入到共用体
semall.array = buff_w;
//利用semctl函数,设置所有信号量的值
ret = semctl(semid,0,SETALL,semall);
if(ret==0)
printf("SETALL success\r\n");
/*获取所有信号量的值--命令:GETALL*/
//定义获取数组
unsigned short buff_r[5]={0};
//将需要获取到的数值传入到数组
semall.array = buff_r;
//利用semctl函数,再获取所有信号量的值
ret = semctl(semid,0,GETALL,semall);
if(ret==0)
{
printf("GETALL success\r\n");
for(int i =0;i<5;i++)
{
printf("buff_r[%d]=%d\r\n",i,buff_r[i]);
}
}
/*对信号量进行PV操作*/
//定义结构体变量-第一个信号量进行PV操作
struct sembuf op[5] = {{0,+10,0},{1,+5,0},{2,-2,0},{3,-5,0},{4,+1,0}};
//调用semop,对信号量的值进行加减操作
ret = semop(semid, op, sizeof(op)/sizeof(op[0]));
if(ret == 0)
{
printf("OP success\r\n");
}
/*获取所有信号量的值--命令:GETALL*/
//定义获取数组
unsigned short buff[5]={0};
//将需要获取到的数值传入到数组
semall.array = buff;
//利用semctl函数,再获取所有信号量的值
ret = semctl(semid,0,GETALL,semall);
if(ret == 0)
{
//查看信号量的值是否进行加减操作
for(int i =0;i<5;i++)
{
printf("buff[%d]=%d\r\n",i,buff[i]);
}
}
//利用semctl函数删除对应的信号量集
ret = semctl(semid,0,IPC_RMID);
if(ret == 0)
printf("RMID success\r\n");
return 0;
}