Linux进程间通信

文章目录

  • 进程通信
  • 管道
    • 无名管道
    • 有名管道
  • 信号通信
    • kill、raise、alarm
    • signal 处理信号
    • 采用信号方式的进程间通信
  • 共享内存
    • shmget 创建
    • ftok 创建key值
    • shmat 映射地址
    • shmdt/shmctl 删除
    • 采用共享内存方式的进程间通信
  • 消息队列
    • msgget 创建
    • msgctl 删除
    • msgsnd 发送消息
    • msgrcv 接收消息
    • 采用消息队列方式的进程间通信
  • 信号灯
    • semget 创建
    • semctl 设置/删除
    • semop 执行p/v操作
    • 采用信号灯方式的进程间通信

进程通信

进程通信在用户空间无法实现,可以通过Linux内核进行通信。线程间通信在用户空间使用全局变量就可以实现。
像下面这样的例子在进程间实现通信是不可行的,子进程中会一直执行while循环而不向下执行,即使父进程中已经改变了全局变量的值。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int global = 0;
int main()
{pid_t pid = fork();if(pid < 0)perror("fork error");else if(pid == 0){while(global == 0);printf("子进程ID: %d\n",getpid());}else{printf("父进程ID: %d\n",getpid());global = 1;}return 0;
}

进程间的通信可以依靠Linux内核来实现,像下面图示这样,一个进程往里面写,一个进程往外面读,这样就实现了进程间通信。
在这里插入图片描述
只有一个Linux内核,进程间的通信方式包括管道通信(有名管道和无名管道)、信号通信、IPC(Inter-Process Communication)通信(共享内存、消息队列、信号灯/量)。Socket通信存在于一个网络中两个进程之间的通信。
每一种通信方式都是基于文件IO的思想。
open用于创建或打开进程通信对象;write用于向进程通信对象中写入内容;read用于从进程通信对象中读取内容;close用于关闭或删除进程通信对象。


管道

Linux下一切皆文件,文件类型包括普通文件、目录文件、字符设备文件、块设备文件、链接文件、管道文件和套接字文件。其中,字符设备文件、块设备文件、管道文件和套接字文件只有节点号,不占磁盘块空间
管道文件是一个特殊的文件,是由队列实现的。对于它的读写也可以使用普通的read、write等函数,但它不是普通文件,并不属于其他任何文件系统,只存在于内存中。
在文件IO中创建一个文件或打开一个文件由open函数实现,它不能创建管道文件,只能用pipe函数来创建管道。

#include <unistd.h>
int pipe(int fd[2]);   //成功返回0,出错返回-1,参数是文件描述符

管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1],其中fd[0]固定用于读管道,而fd[1]固定用于写管道。管道关闭时只需将这两个文件描述符关闭即可,可使用普通的close函数逐个关闭各个文件描述符。
一个管道共享了多对文件描述符时,若将其中的一对读写文件描述符都删除,则该管道就失效。
open函数打开文件读取内容后,内容还是存在的,管道是创建在内存中的,进程结束,空间被释放,管道就不存在了。
无名管道在系统中不存在文件节点,有名管道在系统重存在文件节点。

无名管道

无名管道只能用于具有亲缘关系的进程之间的通信,即父子进程或者兄弟进程之间,它是一个半双工的通信模式,具有固定的读端和写端。
简单的无名管道的代码示例如下。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main()
{int fd[2];int ret = pipe(fd);   //创建无名管道char writebuf[] = "helloworld";char readbuf[50];if(ret < 0){perror("pipe error");exit(1);}printf("fd[0] = %d  fd[1] = %d\n",fd[0],fd[1]);write(fd[1],writebuf,sizeof(writebuf));  //往管道中写read(fd[0],readbuf,sizeof(readbuf));  //从管道中读printf("readbuf = %s\n",readbuf);close(fd[0]);close(fd[1]);  //关闭文件描述符return 0;
}

上面程序编译后的执行结果就是打印出写入管道中的字符串,同时可以发现,pipe函数的返回值是存放在参数中的
在这里插入图片描述
管道中的东西,读完了就删除了,如果管道中没有东西可读,则会读阻塞。
在上面代码的close函数之前再添加下面几行代码。

memset(readbuf,0,128);   //清空数组
read(fd[0],readbuf,sizeof(readbuf));  //从管道中读
printf("readbuf = %s\n",readbuf);

代码编译后执行会发生读阻塞,因为管道中的东西已经被读完了,现在管道已经空了,会出现读阻塞。
在这里插入图片描述
可以看到,第二条打印语句是没有打印的,程序阻塞在了read函数上。
通过循环往管道中写,可以验证内核开辟的的空间写满之后也会发生阻塞。

while(1)
{write(fd[1],writebuf,sizeof(writebuf));  //往管道中写
}
printf("write end!\n");

程序发生写阻塞如下图所示。
在这里插入图片描述
创建管道的函数一定要在创建子进程的函数之前,否则会创建出不同的管道,无法实现通信。
利用无名管道实现进程间通信的简单代码如下。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>int main()
{pid_t pid;int fd[2];int ret = pipe(fd);   //管道在创建子进程之前创建,否则会创建出来两个管道,无法进行通信pid = fork();char writebuf[] = "helloworld";char readbuf[50];if(ret < 0){perror("pipe error");exit(1);}if(pid < 0){perror("fork error");exit(2);}else if(pid == 0){write(fd[1],writebuf,sizeof(writebuf));  //子进程往管道中写printf("子进程pid : %d  ppid : %d\n",getpid(),getppid());printf("子进程写完毕!\n");exit(1);}sleep(1);  //这里可以不等待子进程写完,因为管道为空父进程会阻塞,子进程写入后才会继续向下执行read(fd[0],readbuf,sizeof(readbuf));  //父进程从管道中读printf("父进程pid : %d  ppid : %d\n",getpid(),getppid());printf("父进程读完毕!\n");printf("readbuf = %s\n",readbuf);close(fd[0]);close(fd[1]);  //关闭文件描述符wait(0);return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
利用无名管道实现兄弟进程之间的通信,代码示例如下。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>int main()
{pid_t pid;int fd[2];int ret = pipe(fd);   //管道在创建子进程之前创建,否则会创建出来两个管道pid = fork();char writebuf[] = "helloworld";char readbuf[50];if(ret < 0){perror("pipe error");exit(1);}if(pid < 0){perror("fork error");exit(2);}else if(pid == 0){write(fd[1],writebuf,sizeof(writebuf));  //子进程往管道中写printf("子进程pid : %d  ppid : %d\n",getpid(),getppid());printf("子进程写完毕!\n");exit(1);}printf("父进程pid : %d  ppid : %d\n",getpid(),getppid());pid = fork();  //父进程中再创建子进程if(pid < 0){perror("fork error");exit(2);}else if(pid == 0){sleep(1);read(fd[0],readbuf,sizeof(readbuf));  //兄弟进程从管道中读printf("子进程pid : %d  ppid : %d\n",getpid(),getppid());printf("子进程读完毕!\n");printf("readbuf = %s\n",readbuf);exit(1);}close(fd[0]);close(fd[1]);  //关闭文件描述符wait(0);   //等待任一子进程wait(0);return 0;
}

有名管道

无名管道只能用于具有亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制,它可以使互不相关的两个进程实现彼此通信。该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。不过值得注意的是,FIFO是严格地遵循先进先出规则的,对管道及FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。有名管道的创建可以使用函数mkfifo(),在创建管道成功之后,就可以使用open、read、write这些函数了。
有名管道通过mkfifo函数来创建,其没有在内核中创建管道,只有通过open函数打开这个文件的时候才会在内核空间创建管道。

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode);  //创建成功返回0,失败返回-1

参数mode和umask有关,比如mode设置为0777(八进制),umask为0001,那么最终创建出来的文件权限就是~(mode & umask),即0776。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>int main()
{int ret;ret = mkfifo("./myfifo",0777);if(ret < 0){perror("mkfifo error");exit(1);}printf("mkfifo success!\n");return 0;
}

比如在代码中设置mode为0777,系统的umask为0022,那么最终的文件权限就是0755,如下图所示。
在这里插入图片描述
采用有名管道实现进程间的通信,往管道中写数据的代码如下。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> 
#include <errno.h>int main(int argc,char* argv[])
{int fd;if(argc < 2){fprintf(stderr,"usage : %s string\n",argv[0]);exit(1);}if((mkfifo("./myfifo",O_CREAT|O_EXCL) < 0) && (errno!=EEXIST)){perror("mkfifo error");exit(1);}fd = open("./myfifo",O_WRONLY);if(fd < 0){perror("open myfifo error");exit(1);}write(fd,argv[1],sizeof(argv[1]));   //往管道中写printf("write '%s' in fifo!\n",argv[1]); return 0;
}

从管道中读取数据的代码如下。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main()
{int fd;char readbuf[50];fd = open("./myfifo",O_RDONLY);if(fd < 0){perror("open myfifo error");exit(1);}printf("open myfifo success!\n"); while(1){memset(readbuf,0,sizeof(readbuf));  //清空数组read(fd,readbuf,sizeof(readbuf));   //从管道中读取数据printf("read success from fifo! readbuf = %s\n",readbuf);sleep(3);}return 0;
}

分别编译上面的两段代码后,打开两个终端,一个用来往管道中写数据,一个用来循环从管道中往出读数据,程序执行的结果如下图所示。
在这里插入图片描述


信号通信

信号通信的框架:信号的发送包括kill、raise、alarm,信号的接收包括sleep、while、pause,信号的处理主要是signal函数。
kill函数可以给系统中的进程发送信号,raise函数只能给自身发送信号,alarm函数只能发送闹钟信号,pause函数将当前的进程状态设置为睡眠状态。
不同于管道,信号在内核中已经存在,内核可以发64种不同的信号,如下图所示。
在这里插入图片描述
通过信号通信时,需要告诉内核要发给谁何种信号,比如杀死进程的命令:kill -9 1234,其表示的就是发给进程1234信号9,即杀死该进程。

kill、raise、alarm

kill函数原型及其所需的头文件如下。

#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid,int sig);  //成功返回0,出错返回-1

pid大于0,表示信号发给该进程号表示的进程;pid等于0,信号将被发送到所有和pid进程在同一个进程组的进程;pid为-1,信号将发给所有进程表中的进程,除了进程号最大的进程。
信号通信的简单例子如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>int main(int argc,char* argv[])
{if(argc < 3){fprintf(stderr,"usage : %s signal pid\n",argv[0]);exit(1);}int sig = atoi(argv[1]);int pid = atoi(argv[2]);kill(pid,sig);  //系统调用return 0;
}

运行上面的read程序后,系统将一直循环等待读取,使用自己编写的代码程序实现杀死该进程的过程如下图所示。
在这里插入图片描述
raise发信号给自己,其函数原型及其所需的头文件如下。

#include <signal.h>
#include <sys/types.h>
int raise(int sig);  //成功返回0,出错返回-1
//raise相当于kill(getpid(),int sig)

在主函数中写入下面一行代码。

raise(9);   //向自己发送信号9,即杀死当前进程

程序运行后会被杀死,因为raise函数发了一个杀死自身进程的信号给自己。
在这里插入图片描述
alarm发送闹钟信号SIGALRM,该信号在一个定时器到时后发出,alarm也只能发信号给当前进程。
alarm函数原型及其所需的头文件如下。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);  //成功返回0,出错返回-1,如果在此函数前已经设置了闹钟时间,则返回上一个闹钟的剩余时间
//seconds是设置延迟的秒数

关于alarm函数的使用,代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>int main()
{int second = 0;alarm(5);  //系统调用,设置闹钟时间5秒while(second++ < 10){printf("second = %d \n",second);sleep(1);}return 0;
}

程序运行的结果如下图所示。
在这里插入图片描述

signal 处理信号

信号处理用到的函数是signal,该函数原型及其所需的头文件如下。

#include <signal.h>
void (*signal(int signum,void (*handler)(int)))(int);  //signum是指定的信号
//handler设置为SIG_IGN表示忽略该信号,设置为SIG_DFL表示采用系统默认方式处理信号,也可以自定义函数指针

signal函数传入的参数有两个,一个是要处理的信号,另一个是处理的方式。
在上面alarm函数应用的基础上加入signal函数,代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>void func(int signal)
{for(int i=0;i<3;i++){printf("signal = %d---i = %d\n",signal,i);sleep(1);}
}int main()
{int second = 0;signal(14,func);  //等价于signal(SIGALRM,func);   闹钟信号到时后执行该函数alarm(5);  //系统调用while(second++ < 10){printf("second = %d \n",second);sleep(1);}return 0;
}

SIGALRM代表的是信号14,在程序运行后,先执行主函数中的打印,闹钟到时后执行signal函数中的函数体,函数体执行完成后就返回到原来执行的位置处接着执行,直到程序执行结束。
上面程序的执行结果如下图所示。
在这里插入图片描述
如果在上面程序的alarm函数之后再加入下面一行signal代码,表示忽略该信号,那么程序将不会执行上面signal函数中定义的函数,而是直接运行完主函数内的循环,系统会以最新的一次signal指令为准。

signal(14,SIG_IGN);   //忽略信号;SIG_DFL是默认处理

采用信号方式的进程间通信

父子进程间通过信号通信以及各信号函数的使用示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>void func1(int signum)
{int i = 0;while(i++ < 3){printf("收到SIGUSR1信号,signum = %d  i = %d\n",signum,i);sleep(1);}
}void func2(int signum)
{printf("收到SIGCHLD信号,回收子进程资源,signum = %d\n",signum);wait(0);alarm(3);  //闹钟定时3秒钟
}void func3(int signum)
{printf("收到SIGALRM信号,结束本进程,signum = %d\n",signum);raise(SIGKILL);  //杀死本进程
}int main()
{pid_t pid;pid = fork();if(pid < 0){perror("fork error");exit(1);}else if(pid > 0){int i=1;signal(SIGUSR1,func1);  //等价于signal(10,func1);signal(SIGCHLD,func2);  //等价于signal(17,func1); 父进程接收到子进程退出信号时就回收子进程资源signal(SIGALRM,func3);   //等价于signal(14,func1); 闹钟到时后结束本进程//wait(0);   //直接wait会阻塞while(1){printf("父进程,i = %d \n",i);i++;sleep(1);}}sleep(2);kill(getppid(),SIGUSR1);   //子进程向父进程发送信号sleep(4);  //子进程延迟4秒后退出return 0;  //子进程退出相当于给父进程发送SIGCHLD信号
}

程序编译后的执行结果如下图所示。
在这里插入图片描述
父进程执行死循环,在接收到信号后就跳转到相应的函数中执行,执行后再返回,子进程退出后相当于也给父进程发送了信号,父进程回收子进程资源,防止其成为僵尸进程,回收完后又设置了定时器,到时候杀死本进程。


IPC和文件I/O函数之间的比较如下表所示。

文件I/O消息队列共享内存信号灯
openMsg_getShm_getSem_get
writemsgsndshmatsemop
readmsgrecvshmdtsemop
closemsgctrlshnctrlsemctrl

查看系统中相关内容的命令如下。

ipcs -m //显示共享内存段
ipcrm -m shmid //删除ID为shmid的共享内存段
ipcs -q //显示消息队列
ipcrm -q msgid //删除ID为msgid的消息队列
ipcs -s //显示信号灯阵列
ipcrm -s semid //删除ID为semid的信号灯阵列

共享内存的方式读取完数据后,数据仍然存在,可以多次读取,但是使用消息队列的方式,数据被读取完后就相当于删除了,只能读取一次,和管道是一样的。

共享内存

共享内存创建之后一直存在于内核中,直到被删除或者系统关闭。共享内存不同于管道,其内容被读取之后仍然存在于共享内存中。

shmget 创建

打开或创建一个共享内存对象所使用的shmget函数原型及其所需的头文件如下。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key,int size,int shmflg);  //创建成功返回共享内存段的标识符,出错返回-1

其中,key是IPC_PRIVATE或ftok的返回值;size是共享内存区大小;shmflg和open函数的权限位一致,可使用八进制表示。
创建共享内存的简单代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>int main()
{int shmid;shmid = shmget(IPC_PRIVATE,128,0777);if(shmid < 0){printf("shmget failure!\n");exit(1);}printf("shm = %d\n",shmid);system("ipcs -m");   //显示共享内存段return 0;
}

上面程序运行后的结果如下图所示。
在这里插入图片描述
共享内存实际上就是一段缓存,相当于存在于内核空间的数组。

ftok 创建key值

ftok函数可以用来创建key值,其函数原型如下。

char ftok(const char* path,char key);  //path是文件路径和文件名,key是一个字符
//函数正确执行返回一个key值,失败返回-1

使用ftok函数创建key值时,创建的共享内存key值不再是原来IPC_PRIVATE下的0,如下图所示。
在这里插入图片描述
代码只需要在上面代码的基础上增加下面几行即可。

int key;
key = ftok("./",'a');
//shmid = shmget(IPC_PRIVATE,128,0777);
shmid = shmget(key,128,IPC_CREAT | 0777);

使用IPC_PRIVATE创建和ftok()函数创建key值的区别和无名管道和有名管道的区别一样,使用IPC_PRIVATE创建的共享内存只能在有亲缘关系的进程之间进行通信,而使用ftok()函数创建的共享内存可以在无亲缘关系的进程之间进行通信。

shmat 映射地址

shmat函数将共享内存映射到用户空间地址中,方便用户进程对共享内存的读写操作,其函数原型如下。

void *shmat(int shmid,const void* shmaddr,int shmflg);  //映射成功返回映射后的地址,失败返回NULL
//shmid是共享内存的ID;shmaddr是要映射到的地址,NULL则由系统自动完成映射;shmflg如果是SHM_RDONLY表示共享内存只读,默认是0,表示可读可写

shmat函数的使用示例代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>int main()
{int shmid;char *p;int key;key = ftok("./",'a');//shmid = shmget(IPC_PRIVATE,128,0777);shmid = shmget(key,128,IPC_CREAT | 0777);if(shmid < 0){printf("shmget failure!\n");exit(1);}printf("shm = %d\n",shmid);system("ipcs -m");   //显示共享内存段p = (char *)shmat(shmid,NULL,0);  //将共享内存映射到用户地址if(p == NULL){printf("shamt failure!\n");exit(1);}printf("Input data : ");fgets(p,128,stdin);   //向共享内存写入数据printf("shared memory data : %s",p);   //从共享内存读取数据return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述

shmdt/shmctl 删除

shmdt函数用于将进程里的地址映射删除,函数原型如下。

int shmdt(const void* shmaddr);   //shmaddr是映射后的地址,成功返回0,失败返回-1

映射后的地址也就是shmat函数的返回值。
在上面代码的后面接着添加下面几行代码进行测试,看看删除掉地址映射之后还能不能从原来映射的用户地址中读出数据来。

int ret = shmdt(p);  //删除共享内存在用户空间的地址映射
if(ret < 0)
{printf("shmdt failure!\n");exit(1);
}
printf("shmdt success! ret = %d\n",ret);
printf("shared memory data : %s",p);   //从共享内存读取数据

程序执行结果如下图所示,成功使用shmdt函数删除了映射在用户地址空间的地址,且在删除之后再读取该地址的数据会显示段错误。
在这里插入图片描述
shmctl 函数用于删除共享内存对象,上面的shmdt函数只是删除掉了共享内存在用户空间中的映射,而没有删除掉内核中的共享内存,删除共享内存需要使用函数shmctl,该函数原型如下。

int shmctl(int shmid,int cmd,struct shmid_ds *buf);  //成功返回0,失败返回-1

cmd包括IPC_STAT(获取对象属性)、IPC_SET(设置对象属性)、IPC_RMID(删除对象)。
buf是指定IPC_STAT和IPC_SET时用以保存/设置属性,使用IPC_RMID时,buf设置为NULL。
关于使用shmctl函数删除共享内存的代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>int main()
{int shmid;char *p;int key;key = ftok("./",'a');//shmid = shmget(IPC_PRIVATE,128,0777);shmid = shmget(key,128,IPC_CREAT | 0777);if(shmid < 0){printf("shmget failure!\n");exit(1);}printf("shm = %d\n",shmid);system("ipcs -m");   //显示共享内存段p = (char *)shmat(shmid,NULL,0);  //将共享内存映射到用户地址if(p == NULL){printf("shamt failure!\n");exit(1);}printf("Input data : ");fgets(p,128,stdin);   //向共享内存写入数据printf("shared memory data : %s",p);   //从共享内存读取数据int ret = shmdt(p);   //删除共享内存在用户空间的地址映射if(ret < 0){printf("shmdt failure!\n");exit(1);}printf("shmdt success! ret = %d\n",ret);ret = shmctl(shmid,IPC_RMID,NULL);   //删除内核中的共享内存if(ret < 0){printf("shmctl failure!\n");exit(1);}printf("shmctl success! ret = %d\n",ret);system("ipcs -m"); return 0;
}

程序编译后的执行结果如下图所示,可以看到,共享内存被成功删除了。
在这里插入图片描述
使用该函数和使用命令 ipcrm -m shmid 的功能一样,都是删除掉共享内存。
下面的程序就是实现删除指定共享内存的,用户只要传入要删除的共享内存段号即可。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>int main(int argc,char* argv[])
{if(argc < 2){printf("usage : %s shmid\n",argv[0]);exit(1);}int shmid = atoi(argv[1]);int ret = shmctl(shmid,IPC_RMID,NULL);if(ret != 0){printf("Shared memory delete failure!\n");exit(1);}printf("Shared memory delete success!\n");return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述

采用共享内存方式的进程间通信

父子进程之间采用共享内存通信的代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>void func(int signum)
{return;    //返回后唤醒阻塞函数
}int main()
{pid_t pid;int shmid = shmget(IPC_PRIVATE,128,0777);char *p = (char *)shmat(shmid,NULL,0);if(shmid < 0){printf("shmget failure!\n");exit(1);}pid = fork();  //在创建共享内存的函数之后if(pid < 0){perror("fork error");exit(1);}else if(pid == 0){signal(SIGUSR1,func);  //唤醒pause函数while(1){pause();  //等待父进程写printf("子进程,读取共享内存数据: %s",p);kill(getppid(),SIGUSR2);  //发送信号给父进程,已读完if(strncmp(p,"exit",4) == 0)  //父进程发出exit,子进程就退出循环并结束break;}printf("子进程退出!\n");exit(1);}signal(SIGUSR2,func);while(1){printf("父进程,输入写入共享内存数据: ");fgets(p,128,stdin);   //父进程输入数据kill(pid,SIGUSR1);   //发送信号给子进程,已写完pause();if(strncmp(p,"exit",4) == 0)  //父进程发出exit就退出循环break;}wait(0);shmdt(p);shmctl(shmid,IPC_RMID,NULL);printf("父进程退出!\n");return 0; 
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
父进程写入数据后发送信号给子进程,告诉子进程已经写完,然后阻塞,子进程接收到父进程的信号后解除阻塞,读取共享内存中的数据,然后发信号给父进程告诉自己已经读完,就这样实现了单向通信,如果父进程发送exit,则双方结束通信。
非亲缘关系进程间通信需采用ftok函数生成key,通信双方应在shmget函数中使用相同的key来创建共享内存,为了确保双方通过信号互发消息,在打开共享内存后,写端先将进程号写进共享内存,读端读取进程号后将自己的进程号也写进共享内存,写端从共享内存读出写端的进程号,之后双方就可以开始正式通信了!
写端的代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>struct mybuf
{int mypid;char buf[100];
};void func(int signum)
{return;  
}int main()
{int key = ftok("./",'a');  //通过相同的key确保两个程序中的shmid相同if(key < 0){printf("ftok failure!\n");exit(1);}int shmid = shmget(key,128,IPC_CREAT | 0777);    //非亲缘关系进程间需采用ftok函数生成keyif(shmid < 0){printf("shmget failure!\n");exit(1);}printf("pid : %d shmid : %d\n",getpid(),shmid);struct mybuf *p = (struct mybuf *)shmat(shmid,NULL,0);p->mypid = getpid();   //第一次先将写端的进程号写入共享内存signal(SIGUSR2,func);   //等待读端进程读取到写端pid后唤醒pause();int read_pid = p->mypid;  //获取读端的pid用于发送信号while(1){printf("pid: %d --- 输入写入共享内存数据: ",getpid());fgets(p->buf,128,stdin);   //输入数据kill(read_pid,SIGUSR1);   //给读端进程发信号,写完了pause();if(strncmp(p->buf,"exit",4) == 0)  //进程发出exit就退出循环break;}printf("进程%d退出共享内存通信!\n",getpid());shmdt(p);    //删除用户端地址映射shmctl(shmid,IPC_RMID,NULL);  //删除共享内存 return 0; 
}

读端的代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>struct mybuf
{int mypid;char buf[100];
};void func(int signum)
{return;  
}int main()
{int key = ftok("./",'a');  //通过相同的key确保两个程序中的shmid相同if(key < 0){printf("ftok failure!\n");exit(1);}int shmid = shmget(key,128,IPC_CREAT | 0777);    //非亲缘关系进程间需采用ftok函数生成keyif(shmid < 0){printf("shmget failure!\n");exit(1);}printf("pid : %d shmid : %d\n",getpid(),shmid);struct mybuf *p = (struct mybuf *)shmat(shmid,NULL,0);int write_pid = p->mypid;  //读取写端的pidp->mypid = getpid();   //将读端的进程号写入共享内存kill(write_pid,SIGUSR2);   //给写端进程发信号signal(SIGUSR1,func);while(1){pause();printf("pid: %d --- 读取共享内存数据: %s",getpid(),p->buf);kill(write_pid,SIGUSR2);   //给写端进程发信号,读完了if(strncmp(p->buf,"exit",4) == 0)  //进程收到exit就退出循环break;}printf("进程%d退出共享内存通信!\n",getpid());shmdt(p);  //删除用户端地址映射shmctl(shmid,IPC_RMID,NULL);  //删除共享内存 return 0; 
}

将上面的代码编译后,打开两个终端分别运行可执行程序,非亲缘进程之间利用共享内存通信的结果如下图所示。
在这里插入图片描述
可以看到,利用共享内存实现了非亲缘进程之间的通信,重点是在两个进程中使用由ftok()函数生成的key值创建共享内存,这样创建出来的共享内存段号就是一样的。


消息队列

msgget 创建

msgget()函数用于创建消息队列,其函数原型及其所需的头文件如下。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(ket_t key,int flag);   //成功返回消息队列的ID,失败返回-1

key是和消息队列关联的key值,flag是消息队列的访问权限。
由于消息队列是链式结构,因此不像共享内存一样需要指定大小,消息队列的创建函数只带两个参数。
消息队列创建函数msgget()的使用和共享内存类似,代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int main()
{int msgid;//msgid = msgget(IPC_PRIVATE,0777);int key = ftok("./",'a');msgid = msgget(key,IPC_CREAT | 0777);if(msgid < 0){printf("msgget failure!\n");exit(1);}printf("msgget success! msgid = %d\n",msgid);system("ipcs -q");   //显示消息队列return 0;
}

msgctl 删除

msgctl()函数用于删除指定ID号的消息队列,其函数原型及其所需的头文件如下。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msgid,int cmd,struct msqid_ds *buf);   //成功返回0,失败返回-1

cmd包括IPC_STAT(获取对象属性)、IPC_SET(设置对象属性)、IPC_RMID(删除对象)。
buf是指定IPC_STAT和IPC_SET时用以保存/设置属性,使用IPC_RMID时,buf设置为NULL。
在上面创建代码的基础上加入下面的代码删除消息队列。

msgctl(msgid,IPC_RMID,NULL);

程序的执行结果如下图所示。
在这里插入图片描述
可以看到,使用msgget函数成功创建了消息队列,使用msgctl函数成功删除了消息队列。

msgsnd 发送消息

msgsnd()函数用于向消息队列中写入数据,其函数原型及其所需的头文件如下。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msgid,const void* msgp,size_t size,int flag);   //成功返回0,失败返回-1

msgp是指向消息的指针,常用的消息结构msgbuf如下。

struct msgbuf
{long type;  //消息类型char text[N];  //消息正文
};

size是要发送的消息字节数,flag设置为0表示要等到发送完消息函数才返回,设置为IPC_NOWAIT表示消息没有发送完成,函数也会立即返回。
msgsnd函数发送消息的主要代码如下。

struct msgbuf   //定义结构体
{long type;  //消息类型char text[20];  //消息正文
};struct msgbuf sendbuf;
sendbuf.type = 100;  //设置类型
printf("Please input message : ");
fgets(sendbuf.text,20,stdin);   //通过用户输入写入的数据
msgsnd(msgid,&sendbuf,strlen(sendbuf.text),0);   //发送消息

msgrcv 接收消息

msgrcv()函数用于从消息队列中读出数据,其函数原型及其所需的头文件如下。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msgid,void* msgp,size_t size,long msgtype,int flag);   //成功返回接收消息的长度,失败返回-1

msgp是接收消息的缓冲区;size是要接收的字节数;msgtype置0表示接收消息队列中的第一个消息,大于0就接收消息队列中第一个类型为msgtype的消息,小于0就接收消息队列中类型值不大于msgtype绝对值且类型值最小的消息;flag置0,若无消息就会一直阻塞,设置为IPC_NOWAIT,没有消息进程会立即返回ENOMSG。
因为消息队列是链式队列,在使用msgrcv()函数接收消息的时候会根据消息的类型msgtype去查找。
msgrcv函数接收消息的主要代码如下。

struct msgbuf rcvbuf;
memset(rcvbuf.text,0,20);  //先清理缓存
int ret = msgrcv(msgid,&rcvbuf,20,100,0);   //接收消息,类型参数要和写入的类型一致
printf("read length : %d  rcvbuf.text = %s",ret,rcvbuf.text);

下面的例子是上面介绍到四种操作消息队列函数的综合应用。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>struct msgbuf
{long type;  //消息类型char text[20];  //消息正文
};int main()
{int msgid;msgid = msgget(IPC_PRIVATE,0777);//int key = ftok("./",'a');//msgid = msgget(key,IPC_CREAT | 0777);if(msgid < 0){printf("msgget failure!\n");exit(1);}printf("msgget success! msgid = %d\n",msgid);//system("ipcs -q");   //显示消息队列struct msgbuf sendbuf;sendbuf.type = 100;printf("Please input message : ");fgets(sendbuf.text,20,stdin);   //通过用户输入写入的数据msgsnd(msgid,&sendbuf,strlen(sendbuf.text),0);   //发送消息struct msgbuf rcvbuf;memset(rcvbuf.text,0,20);  //先清理缓存int ret = msgrcv(msgid,&rcvbuf,20,100,0);   //接收消息,类型参数要和写入的类型一致printf("read length : %d  rcvbuf.text = %s",ret,rcvbuf.text);msgctl(msgid,IPC_RMID,NULL);    //删除消息队列return 0;
}

程序编译后的执行结果如下图所示。
在这里插入图片描述

采用消息队列方式的进程间通信

父子进程间通过消息队列的方式通信很简单,对上面的代码简单修改即可,其代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>struct msgbuf
{long type;  //消息类型char text[20];  //消息正文
};void func()
{return;
}int main()
{int msgid;msgid = msgget(IPC_PRIVATE,0777);if(msgid < 0){printf("msgget failure!\n");exit(1);}printf("msgget success! msgid = %d\n",msgid);pid_t pid = fork();if(pid < 0){perror("fork error");exit(1);}else if(pid == 0)   //子进程读{signal(SIGUSR1,func);   //接收到信号后就唤醒子进程,向下执行pause();struct msgbuf rcvbuf;memset(rcvbuf.text,0,20);  //先清理缓存int ret = msgrcv(msgid,&rcvbuf,20,100,0);   //接收消息,类型参数要和写入的类型一致printf("pid : %d  read length : %d  rcvbuf.text = %s",getpid(),ret,rcvbuf.text);msgctl(msgid,IPC_RMID,NULL);  //删除消息队列exit(0);   //子进程退出}struct msgbuf sendbuf;   //父进程写sendbuf.type = 100;printf("pid : %d --- input message : ",getpid());fgets(sendbuf.text,20,stdin);   //通过用户输入写入的数据msgsnd(msgid,&sendbuf,strlen(sendbuf.text),0);   //发送消息kill(pid,SIGUSR1);  //给子进程发信号唤醒wait(0);return 0;
}

程序的运行结果如下图所示。
在这里插入图片描述
非亲缘关系进程之间采用消息队列实现单向通信的代码示例如下。
写端的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define N 50
struct msgbuf
{long type;  //消息类型char text[N];  //消息正文
};int main()
{int key = ftok("./",'a');int msgid = msgget(key,IPC_CREAT | 0777);if(msgid < 0){printf("msgget failure!\n");exit(1);}printf("msgget success! msgid = %d---pid = %d\n",msgid,getpid());struct msgbuf sendbuf; sendbuf.type = 100;  //定义消息类型while(1)  //循环写{memset(sendbuf.text,0,N);  //清理数组缓存printf("Please input message : ");fgets(sendbuf.text,N,stdin);   //通过用户输入写入的数据msgsnd(msgid,&sendbuf,strlen(sendbuf.text),0);   //发送消息if(strncmp(sendbuf.text,"exit",4) == 0)break;}printf("进程%d退出!\n",getpid());msgctl(msgid,IPC_RMID,NULL);  //删除消息队列return 0;
}

读端的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define N 50struct msgbuf
{long type;  //消息类型char text[N];  //消息正文
};int main()
{int key = ftok("./",'a');int msgid = msgget(key,IPC_CREAT | 0777);if(msgid < 0){printf("msgget failure!\n");exit(1);}printf("msgget success! msgid = %d---pid = %d\n",msgid,getpid());struct msgbuf rcvbuf;int ret;while(1){memset(rcvbuf.text,0,N);  //清理缓存ret = msgrcv(msgid,&rcvbuf,N,100,0);   //接收消息,类型参数要和写入的类型一致printf("read length : %d---rcvbuf.text = %s",ret,rcvbuf.text);if(strncmp(rcvbuf.text,"exit",4) == 0)break;}   printf("进程%d退出!\n",getpid());msgctl(msgid,IPC_RMID,NULL);  //删除消息队列return 0;
}

将上面的两个代码程序分别编译后,打开两个终端分别运行两个可执行程序,单向通信的结果如下图所示。
在这里插入图片描述
和共享内存相比较,消息队列之间这种单向的通信比较简单,不涉及两个进程互发信号告诉对方写完或是读完,消息队列中有一个参数就是设置阻塞的,在代码中将flag参数设置为0,另一端就会阻塞等待。
当开一个写终端,两个读终端的时候,两个读终端各读一次,但是谁也不能读取完全写端的内容,这就是消息队列的特点,只能读取一次,读完之后再读就不存在了。
在这里插入图片描述
非亲缘关系进程之间采用消息队列实现双向通信的代码示例如下。
服务器端的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define N 50
struct msgbuf
{long type;  //消息类型char text[N];  //消息正文
};int main()
{int key = ftok("./",'a');int msgid = msgget(key,IPC_CREAT | 0777);if(msgid < 0){printf("msgget failure!\n");exit(1);}printf("msgget success! msgid = %d\n",msgid);int pid = fork();   //实现多进程之间的读写struct msgbuf sendbuf,rcvbuf; sendbuf.type = 100;  //发送消息类型:100,接收消息类型:200if(pid < 0){perror("fork error");exit(1);}else if(pid == 0)  //子进程写{while(1){memset(sendbuf.text,0,N);  //清理数组缓存printf("send pid = %d---please input message : \n",getpid());fgets(sendbuf.text,N,stdin);   //通过用户输入写入的数据msgsnd(msgid,&sendbuf,strlen(sendbuf.text),0);   //发送消息}exit(1);}int ret;while(1)   //父进程读{memset(rcvbuf.text,0,N);  //清理缓存ret = msgrcv(msgid,&rcvbuf,N,200,0);   //接收消息,类型参数要和写入的类型一致printf("receive pid = %d---read length = %d---rcvbuf.text = %s",getpid(),ret,rcvbuf.text);}msgctl(msgid,IPC_RMID,NULL);  //删除消息队列return 0;
}

客户端的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define N 50
struct msgbuf
{long type;  //消息类型char text[N];  //消息正文
};int main()
{int key = ftok("./",'a');int msgid = msgget(key,IPC_CREAT | 0777);if(msgid < 0){printf("msgget failure!\n");exit(1);}printf("msgget success! msgid = %d\n",msgid);int pid = fork();   //实现多进程之间的读写struct msgbuf sendbuf,rcvbuf; sendbuf.type = 200;  //发送消息类型:200,接收消息类型:100if(pid < 0){perror("fork error");exit(1);}else if(pid == 0)  //子进程读{int ret;while(1){memset(rcvbuf.text,0,N);  //清理缓存ret = msgrcv(msgid,&rcvbuf,N,100,0);   //接收消息,类型参数要和写入的类型一致printf("receive pid = %d---read length = %d---rcvbuf.text = %s",getpid(),ret,rcvbuf.text);}   exit(1);}while(1)  //父进程写{memset(sendbuf.text,0,N);  //清理数组缓存printf("send pid = %d---Please input message : \n",getpid());fgets(sendbuf.text,N,stdin);   //通过用户输入写入的数据msgsnd(msgid,&sendbuf,strlen(sendbuf.text),0);   //发送消息}msgctl(msgid,IPC_RMID,NULL);  //删除消息队列return 0;
}

非亲缘进程之间采用消息队列实现双向通信的过程如下图所示。
在这里插入图片描述


信号灯

信号灯中的函数都是对一个集合的操作。

semget 创建

semget()函数用于创建信号灯,其函数原型及其所需的头文件如下。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key,int nsems,int semflg);   //成功返回信号灯集ID,失败返回-1

key是和信号灯关联的key值,nsems是信号灯集中包含的信号灯数目,semflg是信号灯集的访问权限。
创建信号的的代码示例如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int main()
{int semid;semid = semget(IPC_PRIVATE,3,0777);//int key = ftok("./",'a');//semid = semget(key,3,IPC_CREAT | 0777);if(semid < 0){printf("semget failure!\n");exit(1);}printf("semget success! semid = %d\n",semid);system("ipcs -s");   //显示信号灯return 0;
}

将程序编译后执行的结果如下图所示。
在这里插入图片描述

semctl 设置/删除

semctl()函数用于设置或者删除信号灯,其函数原型及其所需的头文件如下。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid,int semnum,int cmd,union semun arg);   //成功返回0,失败返回-1

semid是信号灯集ID,semnum是要修改的信号灯编号,cmd包括GETVAL(获取信号灯的值)、SETVAL(设置信号灯的值)、IPC_RMID(从系统中删除信号灯集合),最后一个参数在删除的时候可有可无,如果有就设置为NULL。
union semun结构体如下。

union semun
{int val;   //设置信号灯的值struct semid_ds *buf;   //获取对象属性或者设置对象属性时存放的值unsigned short *array;struct seminfo *_buf;
};

可以通过在命令行输入man semctl命令查看到,如下图所示。
在这里插入图片描述
删除信号灯集与第二个参数和第四个参数没有关系,在上面代码的基础上添加下面的代码即可实现信号灯集的删除。

semctl(semid,0,IPC_RMID);

semop 执行p/v操作

semop()函数原型如下。

int semop(int semid,struct sembuf *opsptr,size_t nops);    //成功返回0,出错返回-1
//semid是信号灯集ID;nops是要操作的信号灯的个数;struct sembuf结构体如下
struct sembuf
{short sem_num;   //要操作的信号灯编号short sem_op;   //0:等待信号灯的值变为0;1:释放资源,相当于V操作;-1:分配资源,相当于P操作short sem_flg;  //0: IPC_NOWAIT,SEM_UNDO
};

线程同步中使用信号量的例子如下,该例子中确保主线程执行完再执行子线程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>   //信号量头文件sem_t sem;   //创建信号量变量
void* func(void *arg)
{sem_wait(&sem);   //资源数减1,确保主线程先执行for(int i=0;i<5;i++){sleep(1);printf("child thread_id = %ld  i = %d\n",pthread_self(),i);}return NULL;
}int main(int argc,char *argv[])
{sem_init(&sem,0,0);   //第二个参数设置为0表示线程同步,信号量初始化pthread_t tid;int ret = pthread_create(&tid,NULL,func,NULL);  //创建线程if(ret < 0){perror("pthread create error");exit(1);}for(int i=0;i<5;i++){sleep(1);printf("main thread_id = %ld i = %d\n",pthread_self(),i);}sem_post(&sem);   //资源数加1唤醒子线程pthread_join(tid,NULL);   //线程回收sem_destroy(&sem);  //销毁信号量return 0;
}

用信号灯实现与上面程序相同的功能,代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <pthread.h>int semid;
union semun
{int val;   //设置信号灯的值struct semid_ds *buf;   //获取对象属性或者设置对象属性时存放的值unsigned short *array;struct seminfo *_buf;
};
union semun mysemun;// struct sembuf
// {
// 	short sem_num;   //要操作的信号灯编号
// 	short sem_op;   //0:等待信号灯的值变为0;1:释放资源,相当于V操作;-1:分配资源,相当于P操作
// 	short sem_flg;  //0: IPC_NOWAIT,SEM_UNDO
// };
struct sembuf mysembuf;void* func(void *arg)
{mysembuf.sem_op = -1;   //确保主线程先执行,P操作semop(semid,&mysembuf,1);  //操作一个信号灯for(int i=0;i<5;i++){sleep(1);printf("child thread_id = %ld  i = %d\n",pthread_self(),i);}return NULL;
}int main()
{semid = semget(IPC_PRIVATE,3,0777);  //创建了包含3个信号灯的集合if(semid < 0){printf("semget failure!\n");exit(1);}printf("semget success! semid = %d\n",semid);mysemun.val = 0;  //给联合体变量赋值semctl(semid,0,SETVAL,mysemun);  //设置信号灯0的初始值mysembuf.sem_num = 0;   //要操作的信号灯编号mysembuf.sem_flg = 0;  //阻塞操作pthread_t tid;int ret = pthread_create(&tid,NULL,func,NULL);  //创建线程if(ret < 0){perror("pthread create error");exit(1);}for(int i=0;i<5;i++){sleep(1);printf("main thread_id = %ld i = %d\n",pthread_self(),i);}mysembuf.sem_op = 1;   //V操作,让子线程执行semop(semid,&mysembuf,1);pthread_join(tid,NULL);   //线程回收return 0;
}

上面两个程序的执行结果是一样的,都是主线程先执行,然后子线程后执行,如下图所示。
在这里插入图片描述

采用信号灯方式的进程间通信

采用信号灯方式设置两个进程,让服务器端进程先执行打印操作,打印完成后执行V操作唤醒客户端进程,然后客户端再执行打印操作,打印完成后删除信号灯。
服务器端的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semid;
union semun
{int val;   //设置信号灯的值struct semid_ds *buf;   //获取对象属性或者设置对象属性时存放的值unsigned short *array;struct seminfo *_buf;
};
union semun mysemun;
struct sembuf mysembuf;int main()
{int key = ftok("./",'a');if(key < 0){exit(1);}semid = semget(key,3,IPC_CREAT | 0777);  //创建信号灯if(semid < 0){printf("semget failure!\n");exit(1);}printf("semget success! semid = %d\n",semid);// mysemun.val = 0;  //设置信号灯的初始值// semctl(semid,0,SETVAL,mysemun);  //设置信号灯0的值mysembuf.sem_num = 0;   //要操作的信号灯编号mysembuf.sem_flg = 0;  //阻塞操作for(int i=0;i<5;i++)   //先打印{sleep(1);printf("pid = %d i = %d\n",getpid(),i);}mysembuf.sem_op = 1;   //V操作,唤醒有相同semid的另一个进程semop(semid,&mysembuf,1);return 0;
}

客户端的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semid;
union semun
{int val;   //设置信号灯的值struct semid_ds *buf;   //获取对象属性或者设置对象属性时存放的值unsigned short *array;struct seminfo *_buf;
};
union semun mysemun;
struct sembuf mysembuf;int main()
{int key = ftok("./",'a');if(key < 0){exit(1);}semid = semget(key,3,IPC_CREAT | 0777);  //创建信号灯if(semid < 0){printf("semget failure!\n");exit(1);}printf("semget success! semid = %d\n",semid);mysemun.val = 0;  //设置信号灯的初始值semctl(semid,0,SETVAL,mysemun);  //设置信号灯0的值mysembuf.sem_num = 0;   //要操作的信号灯编号mysembuf.sem_flg = 0;  //阻塞操作mysembuf.sem_op = -1;   //P操作,让另一个进程先执行,等待唤醒semop(semid,&mysembuf,1);for(int i=0;i<5;i++)   //后打印{sleep(1);printf("pid = %d i = %d\n",getpid(),i);}semctl(semid,0,IPC_RMID);  //删除信号灯return 0;
}

将上面的两个程序分别编译,然后打开两个终端,在一个终端中先执行客户端程序,因为该程序中执行了信号灯的初始化操作,随后执行服务器端的程序,程序运行的动图如下。
请添加图片描述
由上面的动图可以看出,尽管客户端的程序先运行,但是先执行打印的还是服务器端进程,服务器端打印完成后退出,随后客户端再执行打印操作。


参考视频:
linux进程间通信

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/292857.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言之动态内存管理

在C语言中我们在栈上开辟的空间是固定的&#xff0c;一旦确定好大小就不能随意改变&#xff0c;就想你创建了 int i 10; int arr[10] {0}; int i 一旦确定下来就是四个字节&#xff0c;arr一旦确定好大小在重新运行时也是不能改变的。 为此C语言引入了动态内存空间开辟&#…

java算法day39 | 动态规划part02 ● 62.不同路径 ● 63. 不同路径 II

62.不同路径 思路&#xff1a; 本题非常巧妙。 第一步&#xff1a;定义一个dp数组存储到达每个位置的路径数。 第二步&#xff1a;每个位置的路径数它左面位置的路径数上面位置的路径数。 第三步&#xff1a;不好想的是如何初始化数组。 既然只能向下或向右走&#xff0c;可推出…

全局UI方法-弹窗三-文本滑动选择器弹窗(TextPickDialog)

1、描述 根据指定的选择范围创建文本选择器&#xff0c;展示在弹窗上。 2、接口 TextPickDialog(options?: TextPickDialogOptions) 3、TextPickDialogOptions 参数名称 参数类型 必填 参数描述 rang string[] | Resource 是 设置文本选择器的选择范围。 selected nu…

C易错注意之分支循环,悬空else,短路表达式,static

接下来的日子会顺顺利利&#xff0c;万事胜意&#xff0c;生活明朗-----------林辞忧 前言&#xff1a; c语言中一些关于分支循环中continue常混淆&#xff0c;悬空esle问题&#xff0c;短路表达式&#xff0c;static ,extern在使用时稍不注意就会出错的点,接下来我们将介绍…

javaWeb项目-学生考勤管理系统功能介绍

项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 1、JAVA技术 JavaSc…

Bat中cd到中文路径报错以及windows上设置快捷方式延迟启动执行

场景 要实现在windows启动目录下&#xff0c;执行bat脚本文件。 脚本文件中需要进入某个中文目录 所以直接 cd /d D:\test\中文路径 start test.bat 此时会提示&#xff1a; 此时需要指定bat的编码方式&#xff0c;修改bat脚本文件&#xff0c;添加如下 chcp 65001 cd /d…

AI预测福彩3D第22弹【2024年3月31日预测--第5套算法开始计算第4次测试】

今天&#xff0c;咱们继续进行本套算法的测试&#xff0c;今天为第四次测试&#xff0c;仍旧是采用冷温热趋势结合AI模型进行预测。好了&#xff0c;废话不多说了。直接上结果~ 仍旧是分为两个方案&#xff0c;1大1小。 经过人工神经网络计算并进行权重赋值打分后&#xff0c;3…

通过WSL在阿里云上部署Vue项目

参考&#xff1a; 阿里云上搭建网站-CSDN博客 云服务器重装 关闭当前运行实例 更换操作系统&#xff0c;还有其他的进入方式。 选择ubuntu系统&#xff08;和WSL使用相同的系统&#xff09;。 设置用户和密码。发送短信验证码。 新系统更新。秒速干净的新系统设置完成。 这…

国内ip切换app,让切换ip变得简单

在数字化快速发展的今天&#xff0c;互联网已经成为我们生活中不可或缺的一部分。然而&#xff0c;随着网络应用的深入&#xff0c;用户对于网络环境的需求也日益多样化。其中&#xff0c;IP地址作为网络中的关键标识&#xff0c;其切换与管理显得尤为重要。为了满足用户对于IP…

MongoDB副本集环境搭建(以单机Windows为例)

前言 近期有搭建MongoDB副本集的需求,简单记录一下搭建过程(以本地Windows环境为例)。 一、副本集选型 1 Primary节点、1 Secondary 节点、1 Arbiter节点模式副本集环境搭建。 二、搭建过程 1. 安装MongoDB服务 下载地址:https://www.mongodb.com,如下图所示: 选择…

element表格 加滚动,监听底部实现分页加载

表格要实现滚动很简单&#xff0c;给他加一个高度即可 height"300" 然后是监听事件 mounted() {this.lazyLoading();}, methods:{lazyLoading(){let dom document.querySelector(".el-table__body-wrapper");dom.addEventListener("scroll", (…

数据结构——优先级队列及多服务台模拟系统的实现

一、优先级队列的定义和存储 优先级队列定义&#xff1a;优先级高的元素在队头&#xff0c;优先级低的元素在队尾 基于普通线性表实现优先级队列&#xff0c;入队和出队中必有一个时间复杂度O(n),基于二叉树结构实现优先级队列&#xff0c;能够让入队和出队时间复杂度都为O(log…

2024年【N1叉车司机】考试技巧及N1叉车司机复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 N1叉车司机考试技巧参考答案及N1叉车司机考试试题解析是安全生产模拟考试一点通题库老师及N1叉车司机操作证已考过的学员汇总&#xff0c;相对有效帮助N1叉车司机复审考试学员顺利通过考试。 1、【多选题】《中华人民…

基于springboot+vue实现的学校田径运动会管理系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

开源AI引擎:文本自动分类在公安及消防执法办案自动化中的应用

一、实际案例介绍 通过文本分类算法自动化处理文本数据&#xff0c;快速识别案件性质和关键特征&#xff0c;极大地提高了案件管理和分派的效率。本文将探讨这两种技术如何帮助执法机构优化资源分配&#xff0c;确保案件得到及时而恰当的处理&#xff0c;并增强公共安全管理的…

图论-最短路

一、不存在负权边-dijkstra算法 dijkstra算法适用于这样一类问题&#xff1a; 从起点 start 到所有其他节点的最短路径。 其实求解最短路径最暴力的方法就是使用bfs广搜一下&#xff0c;但是要一次求得所有点的最短距离我们不可能循环n次&#xff0c;这样复杂度太高&#xf…

第十五届蓝桥杯第三期模拟赛第十题 ← 上楼梯

【问题描述】 小蓝要上一个楼梯&#xff0c;楼梯共有 n 级台阶&#xff08;即小蓝总共要走 n 级&#xff09;。小蓝每一步可以走 a 级、b 级或 c 级台阶。 请问小蓝总共有多少种方案能正好走到楼梯顶端&#xff1f;【输入格式】 输入的第一行包含一个整数 n 。 第二行包含三个整…

DeviceNET转EtherCat:水处理行业新神器

在现代水处理行业&#xff0c;自动化控制系统的运用日益广泛。特别是上位机通过DeviceNET转EtherCAT网关的技术&#xff0c;以其高速、高效和精准的特点&#xff0c;正逐步成为水处理自动化控制的主流解决方案。我们需要了解什么是上位机、DeviceNET和EtherCAT。上位机通常指的…

[蓝桥杯 2019 省赛 AB] 完全二叉树的权值

# [蓝桥杯 2019 省 AB] 完全二叉树的权值 ## 题目描述 给定一棵包含 $N$ 个节点的完全二叉树&#xff0c;树上每个节点都有一个权值&#xff0c;按从上到下、从左到右的顺序依次是 $A_1,A_2, \cdots A_N$&#xff0c;如下图所示&#xff1a; 现在小明要把相同深度的节点的权值…

【学习】软件科技成果鉴定测试有何作用

软件科技成果鉴定测试是针对软件进行项目申报、科技成果鉴定等相关目的进行的测试。软件测试报告可作为项目申报、科技成果鉴定等工作的依据之一。软件类科技成果鉴定测试从软件文档、功能性、使用技术等方面对软件系统进行符合性测试。其测试结果证明软件的质量是否符合技术合…