Linux进程间通信

进程通信

进程通信在用户空间无法实现,可以通过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函数生成key
    if(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函数生成key
    if(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;  //读取写端的pid
    p->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 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 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,接收消息类型:200
    if(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,接收消息类型:100
    if(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进程间通信