linux c编程:进程间通信
进程间的通信包括管道,共享内存,信号量通信,消息队列,套借口(socket)和全双工管道通信
首先来看下管道的用法:管道顾名思义,就如同下水道管道一样,当从管道一端流水到另一端的时候,水流的方向是单方向的。某一时刻只能从单方向传递数据,不能双向传递。这种就叫单双工模式。半双工模式只能是一端写数据,一端读数据。来看一个半双工的例子:
(1)在父进程中通过pipe()函数创建一个管道。产生一个描述符,fd[0]指向管道的读端,fd[1]指向管道的写端
(2) 在父进程中调用fork函数产生一个子进程。子进程和父进程都分别指向fd的读端和写端
(3) 在子进程中,关闭fd[0]也就是读的端口,往fd[1]写端口写入数据,在父进程中关闭写端口fd[1],从读端口fd[0]中读取数据并显示在终端上。在父进程中调用了sleep(2)等待2秒的函数,目的是等待子进程写数据
int pipe_function()
{
int fd[2],pid,line;
char message[100];
if (pipe(fd) == -1)
{
perror("not create a new process!");
return 1;
}
else if((pid=fork())<0)
{
perror("create new process fail!");
return 1;
}
else if(pid==0)
{
close(fd[0]);
printf("Child process send message\n");
write(fd[1],"Welcome to microsoft!",19);
}
else
{
close(fd[1]);
sleep(2);
printf("Parent process receive message is:\n");
line=read(fd[0],message,100);
write(STDOUT_FILENO,message,line);
printf("\n");
wait(NULL);
exit(0);
}
return 0;
}
运行结果:
root@zhf-linux:/home/zhf/zhf/c_prj# ./test3
Child process send message
Parent process receive message is:
Welcome to microsof
上面的例子演示了单向通信,如果我们想得到双向通信,父进程在读的同时也给子进程写。要实现这样的功能,我们就必须建立2个管道,一个管道分别是从父进程流向子进程,一个管道是从子进程流向父进程。代码实现如下:定义了两个管道fd和fd1。
int pipe_function_multiy()
{
int fd[2],pid,line,fd1[2],line1;
char message[100],message1[100];
if (pipe(fd) == -1 || pipe(fd1) == -1)
{
perror("not create a new process!");
return 1;
}
else if((pid=fork())<0)
{
perror("create new process fail!");
return 1;
}
else if(pid==0)
{
close(fd[0]);
printf("Child process send message\n");
write(fd[1],"Welcome to microsoft!",19);
sleep(2);
close(fd1[1]);
printf("Child process receive message is:\n");
line1=read(fd1[0],message1,100);
write(STDOUT_FILENO,message1,line1);
printf("\n");
exit(0);
}
else
{
close(fd[1]);
sleep(2);
printf("Parent process receive message is:\n");
line=read(fd[0],message,100);
write(STDOUT_FILENO,message,line);
printf("\n");
close(fd1[0]);
printf("parent process send message is:\n");
write(fd1[1],"hello how are you!",20);
exit(0);
}
return 0;
}
执行结果:
root@zhf-linux:/home/zhf/zhf/c_prj# ./test3
Parent process receive message is:
Child process send message
Welcome to microsof
parent process send message is:
Child process receive message is:
hello how are you!
在上面的例子中,管道只能在有关联的进程中进行,也就是父子进程。那如果不相关的两个进程也需要进行通信该如何解决呢。这里就要到命令管道。通常称为FIFO。通过这个名称可以知道命令管道遵循先进先出的原则。创建一个命令管道有两种方法一种是通过函数创建命名管道,一种是通过shell命令来创建。
首先来看下通过shell命令创建:
(一)首先通过mkfifo创建一个管道文件test
root@zhf-linux:/home/zhf/zhf# mkfifo test
(二)在一个终端中通过cat命令来查看这个命令管道中的数据。由于没有任何写入的数据,因此一直处于等待。这个时候cat命令将一直挂起,直到终端或者有数据发送到FIFO中。
root@zhf-linux:/home/zhf/zhf# cat ./test
(三)然后打开另外一个终端,向FIFO中写入数据
root@zhf-linux:/home/zhf/zhf# echo "hello fifo" > ./test
(四)这个时候cat命令将会输出内容
root@zhf-linux:/home/zhf/zhf# cat ./test
hello fifo
那接下来我们看下通过C代码的方式来实现命令管道的方法:
首先创建2个文件。代表2个进程,一个读,一个写。还是用刚才创建的test这个命令管道文件
test3.c
int mkfifo_function_a()
{
int fd;
int pid;
fd=open(FIFO,O_RDWR);
printf("write the message:\n");
write(fd,"hello world",20);
close(fd);
}
test4.c
int mkfifo_function_b()
{
char msg[100];
int fd;
int pid;
int line;
fd=open(FIFO,O_RDWR);
printf("read the message:\n");
line=read(fd,msg,100);
close(fd);
write(STDOUT_FILENO,msg,line);
}
flags=O_RDONLY:open将会调用阻塞,除非有另外一个进程以写的方式打开同一个FIFO,否则一直等待。
flags=O_WRONLY:open将会调用阻塞,除非有另外一个进程以读的方式打开同一个FIFO,否则一直等待。
flags=O_RDONLY|O_NONBLOCK:如果此时没有其他进程以写的方式打开FIFO,此时open也会成功返回,此时FIFO被读打开,而不会返回错误。
flags=O_WRONLY|O_NONBLOCK:立即返回,如果此时没有其他进程以读的方式打开,open会失败打开,此时FIFO没有被打开,返回-1
(一)在一个终端中运行test4.c。显示读取数据。由于没有输入数据,因此一直挂起
root@zhf-linux:/home/zhf/zhf/c_prj# ./test4
read the message:
(二) 在另外一个终端中运行test3.c。写入数据
root@zhf-linux:/home/zhf/zhf/c_prj# ./test3
write the message:
(三)此时显示出读取信息:
root@zhf-linux:/home/zhf/zhf/c_prj# ./test4
read the message:
hello world
共享内存:
前面介绍到父子进程分别是访问不同的内存,因为子进程拷贝了另外一份内存地址。那么如果想两个进程访问同一块内存地址的数据,就需要用到共享内存了。先来看几个共享内存的函数:
shmget: 创建一块共享内存区域。如果已存在这一块的共享内存。该函数可以打开这个已经存在的共享内存。原型为:
int shmget(key_t key, size_t size, int shmflg);
第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值),只有shmget()函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。
第二个参数,size以字节为单位指定需要共享的内存容量
第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。
shmat函数:
shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
第一个参数,shm_id是由shmget()函数返回的共享内存标识。
第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
第三个参数,shm_flg是一组标志位,通常为0。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
shmdt函数:
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:
int shmdt(const void *shmaddr);
参数shmaddr是shmat()函数返回的地址指针,调用成功时返回0,失败时返回-1.
shmctl函数:用来控制共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);
第一个参数,shm_id是shmget()函数返回的共享内存标识符。
第二个参数,command是要采取的操作,它可以取下面的三个值 :
-
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
-
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
-
IPC_RMID:删除共享内存段
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。
下面来看下一个简单的父子进程访问共享内存的方法:
int share_memory_function()
{
int shmid;
int proj_id;
key_t key;
int sizz;
char *addr;
pid_t pid;
key=IPC_PRIVATE;
shmid=shmget(key,1024,IPC_CREAT|0660);
if (shmid == -1)
{
perror("create share memory failed!");
return 1;
}
addr=(char *)shmat(shmid,NULL,0);
if(addr == (char *)(-1))
{
perror("can not attach");
return 1;
}
strcpy(addr,"welcome to ubuntun");
pid=fork();
if (pid == -1)
{
perror("error!");
return 1;
}
if (pid == 0)
{
printf("Child process string is %s\n",addr);
exit(0);
}
else
{
wait(NULL);
printf("Parent process string is %s\n",addr);
if (shmdt(addr) == 1)
{
perror("release failed");
return 1;
}
if (shmctl(shmid,IPC_RMID,NULL)==-1)
{
perror("failed");
return 1;
}
}
return 0;
}
运行结果:root@zhf-linux:/home/zhf/zhf/c_prj# ./test4Child process string is welcome to ubuntunParent process string is welcome to ubuntun在共享内存中,只要获取到了共享内存的地址,如何进程都可以操作内存,这样就会导致一个问题,多个进程对内存中的变量进行读写。从而使得变量的值变得不可控。信号量就可以解决这个问题。当有一个进程要求使用某一个共享内存中的资源时,系统会首先判断该资源的信号量,如果大于0,则可以使用该资源,并且信号量要减一,当不再使用该资源的时候,信号量再加一。如果信号量等于0,就进入了休眠状态。资源不可访问。来看下信号量的使用:1、创建信号量
semget函数创建一个信号量集或访问一个已存在的信号量集。
#include <sys/sem.h>
int semget (key_t key, int nsem, int oflag) ;
返回值是一个称为信号量标识符的整数,semop和semctl函数将使用它。
参数nsem指定集合中的信号量数。(若用于访问一个已存在的集合,那就可以把该参数指定为0)
参数oflag可以是SEM_R(read)和SEM_A(alter)常值的组合。(打开时用到),也可以是IPC_CREAT或IPC_EXCL ;
2、打开信号量
使用semget打开一个信号量集后,对其中一个或多个信号量的操作就使用semop(op--operate)函数来执行。
#include <sys/sem.h>
int semop (int semid, struct sembuf * opsptr, size_t nops) ;
参数opsptr是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:
struct sembuf{
short sem_num; //
除非使用一组信号量,否则它为
0
short sem_op; //
信号量在一次操作中需要改变的数据,通常是两个数,
//
一个是
-1
,即
P
(等待)操作,一个是
+1
,即
V
(发送信号)操作
short sem_flg; //
通常为
SEM_UNDO,
使操作系统跟踪信号,并在进程没有释放该信号量而终止时,
//
操作系统释放信号量
};
◆参数nops规定opsptr数组中元素个数。
sem_op值:
(1)若sem_op为正,这对应于进程释放占用的资源数。sem_op值加到信号量的值上。(V操作)
(2)若sem_op为负,这表示要获取该信号量控制的资源数。信号量值减去sem_op的绝对值。(P操作)
(3)若sem_op为0,这表示调用进程希望等待到该信号量值变成0
◆如果信号量值小于sem_op的绝对值(资源不能满足要求),则:
(1)若指定了IPC_NOWAIT,则semop()出错返回EAGAIN。
(2)若未指定IPC_NOWAIT,则信号量的semncnt值加1(因为调用进程将进 入休眠状态),然后调用进程被挂起直至:①此信号量变成大于或等于sem_op的绝对值;②从系统中删除了此信号量,返回EIDRM;③进程捕捉到一个信 号,并从信号处理程序返回,返回EINTR。(与消息队列的阻塞处理方式 很相似)
3、信号量操作
semctl函数对一个信号量执行各种控制操作。
#include <sys/sem.h>
int semctl (int semid, int semnum, int cmd, /*
可选参数
*/ ) ;
第四个参数是可选的,取决于第三个参数cmd。
参数semnum指定信号集中的哪个信号(操作对象)
参数cmd指定以下10种命令中的一种,在semid指定的信号量集合上执行此命令。
IPC_STAT 读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET 设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID 将信号量集从内存中删除。
GETALL 用于读取信号量集中的所有信号量的值。
GETNCNT 返回正在等待资源的进程数目。
GETPID 返回最后一个执行semop操作的进程的PID。
GETVAL 返回信号量集中的一个单个的信号量的值。
GETZCNT 返回这在等待完全空闲的资源的进程数目。
SETALL 设置信号量集中的所有的信号量的值。
SETVAL 设置信号量集中的一个单独的信号量的值。
下面来看个具体的应用。首先在/home/zhf/zhf/c_prj/test1.c上创建一个信号量,在模拟系统分配资源。没隔3秒钟就有一个资源被占用。
Test6.c中的代码:
#define RESOURCE 4
int sem_function_set()
{
key_t key;
int semid;
struct sembuf sbuf={0,-1,IPC_NOWAIT};
union semun arg;
if ((key=ftok("/home/zhf/zhf/c_prj/test1.c",'c'))==-1)
{
perror("ftok error!\n");
exit(1);
}
if ((semid=semget(key,1,IPC_CREAT|0666))==-1)
{
perror("segmet error!\n");
exit(1);
}
arg.val=RESOURCE;
if(semctl(semid,0,SETVAL,arg)==-1)
{
perror("semctl errror!\n");
exit(1);
}
while(1)
{
if (semop(semid,&sbuf,1)==-1)
{
perror("semop error!\n");
exit(1);
}
sleep(3);
}
semctl(semid,0,IPC_RMID,0);
exit(0);
}test5.c中的代码:
int sem_get()
{
key_t key;
int semid,semval;
union semun arg;
if((key=ftok("/home/zhf/zhf/c_prj/test1.c",'c'))==-1)
{
perror("key errror1\n");
return 1;
}
if((semid=semget(key,1,IPC_CREAT|0666))==-1)
{
perror("semget error!\n");
return 1;
}
while(1)
{
if((semval=semctl(semid,0,GETVAL,0))==-1)
{
perror("semctl error!\n");
exit(1);
}
if(semval > 0)
{
printf("%d resource could be used\n",semval);
}
else
{
printf("no resource could be used\n");
break;
}
sleep(3);
}
exit(0);
}
分别在2个终端执行:结果如下
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架