【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第二篇 Linux系统编程篇-第三十五章 进程间通信

i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、

【公众号】迅为电子

【粉丝群】258811263(加群获取驱动文档+例程)


第三十五章 进程间通信

进程间的通信应用也是很广泛的,比如后台进程和GUI界面数据传递,发送信号关机,Ctrl+C终止正在运行的程序等。

Linux进程间通信机制分三类:数据交互,同步,信号。理解了这些机制才能灵活运用操作系统提供的IPC工具。

本章以常用的管道(包括有名管道和无名管道),System V IPC(消息队列,共享内存,信号灯),套接字(UNIX域套接字和网络套接字)为例来说明Linux进程通信常用的方法,本文档中介绍的只是一小部分,如果想深入了解可以去翻看专业的书籍。

35.1 无名管道

本章内容对应视频讲解链接(在线观看):

管道通信之无名管道  https://www.bilibili.com/video/BV1zV411e7Cy?p=24

无名管道是最古老的进程通信方式,有如下两个特点:

  1. 只能用于有关联的进程间数据交互,如父子进程,兄弟进程,子孙进程,在目录中看不到文件节点,读写文件描述符存在一个int型数组中。
  2. 只能单向传输数据,即管道创建好后,一个进程只能进行读操作,另一个进程只能进行写操作,读出来字节顺序和写入的顺序一样。

函数

int pipe(int pipefd[2])

头文件

#include <unistd.h>

参数pipefd[2]

一个int型数组,表示管道的文件描述符,pipefd[0]为读,pipefd[1]为写,如下图所示:

返回值

成功返回0,失败返回-1

功能

创建无名管道

 

无名管道使用步骤:

  1. 调用pipe()创建无名管道;
  2. fork()创建子进程,一个进程读,使用read(),一个进程写,使用write()。

实验代码

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\19”目录下。

实现子进程和父进程之间的通信,创建无名管道,父进程从终端获取数据,写入管道,子进程从管道读数据并打印出来。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(void)
{
    char buf[32] = {0};
    pid_t pid;
    // 定义一个变量来保存文件描述符
    // 因为一个读端,一个写端,所以数量为2个
    int fd[2];
    // 创建无名管道
    pipe(fd);
    printf("fd[0] is %d\n", fd[0]);
    printf("fd[2] is %d\n", fd[1]);
    // 创建进程
    pid = fork();
    if (pid < 0)
    {
        printf("error\n");
    }
    if (pid > 0)
    {
        int status;
        close(fd[0]);
        write(fd[1], "hello", 5);
        close(fd[1]);
        wait(&status);
        exit(0);
    }
    if (pid == 0)
    {
        close(fd[1]);
        read(fd[0], buf, 32);
        printf("buf is %s\n", buf);
        close(fd[0]);
        exit(0);
    }

    return 0;
}

在Ubuntu上编译运行,如下图所示:

35.2 有名管道

本章内容对应视频讲解链接(在线观看):

管道通信之有名管道  https://www.bilibili.com/video/BV1zV411e7Cy?p=25

有名管道中可以很好地解决在无关进程间数据交换的要求,并且由于它们是存在于文件系统中的,这也提供了一种比匿名管道更持久稳定的通信办法。有名管道在一些专业书籍中叫做命名管道,它的特点是

1.可以使无关联的进程通过fifo文件描述符进行数据传递;

2.单向传输有一个写入端和一个读出端,操作方式和无名管道相同。

我们使用mkfifo()函数创建有名管道。函数详解如下所示:

函数

int mkfifo(const char *pathname, mode_t mode)

头文件

#include <sys/types.h>

#include <sys/stat.h>

参数pathname

有名管道的路径和名称

参数mode

权限

返回值

成功返回0,失败返回-1

有名管道使用步骤:

  1. 使用mkfifo()创建fifo文件描述符。
  2. 打开管道文件描述符。
  3. 通过读写文件描述符进行单向数据传输。

使用命令创建管道文件

输入以下命令创建管道文件,并查看,如下图所示:

mkfifo fifo

ls

ls -al

 

实验代码

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\20”目录下。

创建两个无关联的进程,一个进程创建有名管道并写数据,另一个进程通过管道读数据。

fifo_write.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int ret;
    char buf[32] = {0};
    int fd;
    if (argc < 2)
    {
        printf("Usage:%s <fifo name> \n", argv[0]);
        return -1;
    }

    if (access(argv[1], F_OK) == 1)
    {
        ret = mkfifo(argv[1], 0666);
        if (ret == -1)
        {
            printf("mkfifo is error \n");
            return -2;
        }
        printf("mkfifo is ok \n");
    }
    fd = open(argv[1], O_WRONLY);
    while (1)
    {
        sleep(1);
        write(fd, "hello", 5);
    }
    close(fd);
    return 0;
}

fifo_read.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
    char buf[32] = {0};
    int fd;
    if (argc < 2)
    {
        printf("Usage:%s <fifo name> \n", argv[0]);
        return -1;
    }
    fd = open(argv[1], O_RDONLY);
    while (1)
    {
        sleep(1);
        read(fd, buf, 32);
        printf("buf is %s\n", buf);
        memset(buf, 0, sizeof(buf));
    }
    close(fd);
    return 0;
}

在Ubuntu下编译 fifo_read.c,并运行如下图所示:

然后重新打开一个窗口,编译fifo_write.c并运行如下图所示: 

fiforead进程可以看到从管道读出的数据: 

35.3 信号

35.3.1 信号发送

本章内容对应视频讲解链接(在线观看):

信号通信(一)  https://www.bilibili.com/video/BV1zV411e7Cy?p=26

信号是Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会执行相应的操作。

信号的产生有三种方式:

1)由硬件产生,如从键盘输入Ctrl+C可以终止当前进程

2)由其他进程发送,如可在shell进程下,使用命令 kill -信号标号 PID,向指定进程发送信号。

3)异常,进程异常时会发送信号

本章只关注在应用层对信号的处理。在Ubuntu终端输入kill -l,查看所有的信号。

 1) SIGHUP  2) SIGINT  3) SIGQUIT  4) SIGILL  5) SIGTRAP

 6) SIGABRT  7) SIGBUS  8) SIGFPE  9) SIGKILL 10) SIGUSR1

11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3

信号

说明

默认操作

SIGABRT

由abort()发送

终止且进行内存转储

SIGALRM

由alarm()发送

终止

SIGBUS

硬件或对齐错误

终止且进行内存转储

SIGCHLD

子进程终止

忽略

SIGCONT

进程停止后继续执行

忽略

SIGFPE

算术异常

终止且进行内存转储

SIGHUP

进程的控制终端关闭(最常见的是用户登出)

终止

SIGILL

进程试图执行非法指令

终止且进行内存转储

SIGINT

用户产生中断符(Ctrl-C)

终止

SIGIO

异步IO事件(Ctrl-C)

终止(a)

SIGKILL

不能被捕获的进程终止信号

终止

SIGPIPE

向无读取进程的管道写入

终止

SIGPROF

向无读取进程的管道写入

终止

SIGPWR

断电

终止

SIGQUIT

用户产生退出符(Ctrl-\)

终止且进行内存转储

SIGSEGV

无效内存访问

终止且进行内存转储

SIGSTKFLT

协处理器栈错误

终止(b)

SIGSTOP

挂起进程

停止

SIGSYS

进程试图执行无效系统调用

终止且进行内存转储

SIGTERM

可以捕获的进程终止信号

终止

SIGTRAP

进入断点

终止且进行内存转储

SIGSTP

用户生成挂起操作符(Ctrl-Z)

停止

SIGTTIN

后台进程从控制终端读

停止

SIGTTOU

后台进程向控制终端写

停止

SIGURG

紧急I/O未处理

忽略

SIGUSR1

进程自定义的信号

终止

SIGUSR2

进程自定义的信号

终止

SIGVTALRM

用ITIMER_VIRTUAL为参数调用setitimer()时产生

终止

SIGWINCH

控制终端窗口大小改变

忽略

SIGXCPU

进程资源超过限制

终止且进行内存转储

SIGXFSZ

文件资源超过限制

终止且进行内存转储

下面是几个常用的函数: 

函数

int kill(pid_t pid, int sig)

头文件

#include <sys/types.h>

#include <signal.h>

参数pid

大于0,时为向PID为pid的进程发送信号

等于0,向同一个进程组的进程发送信号;

等于-1,除发送进程自身外,所有进程ID大于1的进程发送信号。

小于-1,向组ID等于该pid绝对值的进程组内所有进程发送信号。

参数sig

设置发送的信号;等于0时为空信号,无信号发送。常用来进行错误检查。

返回值

执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno

功能

用于向任何进程组或进程发送信号。

函数

int raise(int sig)

头文件

#include <signal.h>

参数sig

信号

功能

相当于kill(getpid(),sig),向进程自身发送信号

函数

unsigned int alarm(unsigned int seconds)

头文件

#include <unistd.h>

参数

设定的时间

功能

设定的时间超过后产生SIGALARM信号,默认动作是终止进程。

注意

每个进程只能有一个alarm()函数,时间到后要想再次使用要重新注册。

使用规则:

实验1代码:在程序中实现:自己给自己发送信号。

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\21”目录下。

raise.c: 

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

int main(void)
{
    printf("raise before\n");
    raise(9);
    printf("raise after\n");
    return 0;
}     

编译运行,如下图所示:

实验2代码kill.c 发送信号。

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\21”目录下。

kill.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

int main(int argc,char *argv[])
{
    pid_t pid;
    int sig;
    if(argc < 3){
        printf("Usage:%s <pid_t> <signal>\n",argv[0]);
        return -1;
    }
    sig = atoi(argv[2]);
    pid = atoi(argv[1]);
    kill(pid,sig);
    return 0;
}

test.c

#include <stdio.h>
#include <unistd.h>

void main(void)
{
    while(1){
        sleep(1);
        printf("hello world\n");        
    }   
}

编译运行test,如下图所示,进程会循环打印hello world。

重新打开另一个窗口,编译kill.c,然后查看test进程的pid号,运行测试如下图所示; 

与此同时,显示test的窗口显示,test进程被杀死,如下图所示: 

实验3代码alarml.c  

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

int main(int argc,char *argv[])
{
    int i;
    alarm(3);
    while(1){
        sleep(1);
        i++;
        printf("i = %d\n",i);
    }
    return 0;
}   

编译alarm.c,并运行。如下图所示,设定的时间(3秒)超过后产生SIGALARM信号,默认动作是终止进程。

35.3.2 信号接收

本章内容对应视频讲解链接(在线观看):

信号通信(二)  https://www.bilibili.com/video/BV1zV411e7Cy?p=27

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\22”目录下。

接收信号:如果要让我们接收信号的进程可以接收到信号,那么这个进程就不能停止。让进程不停止有三种方法:

  1. while
  2. sleep
  3. pause

方法一;

while.c 

#include <stdio.h>
#include <unistd.h>

void main(void)
{
    while(1){
        sleep(1);
        printf("hello world\n");        
    }   
}   

编译运行结果如下所示,按ctrl+C会发送SIGINT信号:

方法二;

sleep.c

#include <stdio.h>
#include <unistd.h>

void main(void)
{
    sleep(60);  
}   

编译运行,如下图所示,会休眠60s.

方法三

使用pause()函数,函数详解如下:

函数

int pause(void)

头文件

#include <unistd.h>

返回值

进程被信号中断后一直返回-1

功能

将进程挂起,等待信号

 pause.c

#include <stdio.h>
#include <unistd.h>

void main(void)
{
    printf("pause before\n");
    pause();
    printf("pause after\n");
}   

编译程序并运行,如下图所示:

输入以下命令查看进程,如下图所示: 

按ctrl+C键,pause进程终止,再次查看pause的进程,如下图所示: 

35.3.3 信号处理

本章内容对应视频讲解链接(在线观看):

信号通信(三)  https://www.bilibili.com/video/BV1zV411e7Cy?p=28

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\23”目录下。

信号是由操作系统来处理的,说明信号的处理在内核态。信号不一定会立即被处理,此时会储存在信号的信号表中。

处理过程示意图: 

由上图中可看出信号有三种处理方式:

1.默认方式(通常是终止进程),

2.忽略,不进行任何操作。

3.捕捉并处理调用信号处理器(回调函数形式)。

函数

sighandler_t signal(int signum, sighandler_t handler);

可以简化成signal(参数1,参数2);

头文件

#include <unistd.h>

typedef void (*sighandler_t)(int);

参数1

我们要进行处理的信号,系统的信号我们可以在终端键入kill -l查看。

参数2

处理的方式(是系统默认还是忽略还是捕获)

忽略该信号,填写“SIG_IGN”;

采用系统默认方式处理信号填写“SIG_DFL”;

捕获到信号后执行此函数内容,

定义格式为“typedef void (*sighandler_t)(int)”,sighandler_t代表一个函数指针。

返回值

调用成功返回最后一次注册信号调用signal()时的handler值失败返回SIG_ERR。

功能

改变收到信号后的动作。

实验1 代码实现信号忽略 

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(void)
{
    signal(SIGINT,SIG_IGN);
    while(1){
        printf("wait signal\n");
        sleep(1);
    }
    return 0;
}

编译运行程序,如下图所示,当我们按下ctrl+C键的时候,信号被忽略。

实验2:代码实现采用系统默认方式处理信号

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(void)
{
    signal(SIGINT,SIG_DFL);
    while(1){
        printf("wait signal\n");
        sleep(1);
    }

    return 0;
}

实验3 代码实现捕获到信号后执行此函数内容

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void myfun(int sig)
{
    if(sig == SIGINT){
        printf("get sigint\n");
    }
}
int main(void)
{
    signal(SIGINT,myfun);
    while(1){
        sleep(1);
        printf("wait signal\n");
    }

    return 0;
}

编译运行程序如下图所示,当我们按下ctrl+c时,显示myfun函数里面的打印信息。

我们再打开另一个终端,输入如下图所示的命令也可以实现同样的效果。

35.4 共享内存

本章内容对应视频讲解链接(在线观看):

共享内存  https://www.bilibili.com/video/BV1zV411e7Cy?p=29

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\24”目录下。

共享内存,顾名思义就是允许两各不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

Linux操作系统的进程通常使用的是虚拟内存,虚拟内存空间是有由物理内存映射而来的。System V 共享内存能够实现让两个或多个进程访问同一段物理内存空间,达到数据交互的效果。

 

共享内存和其他进程间数据交互方式相比,有以下几个突出特点:

  1. 速度快,因为共享内存不需要内核控制,所以没有系统调用。而且没有向内核拷贝数据的过程,所以效率和前面几个相比是最快的,可以用来进行批量数据的传输,比如图片。
  2. 没有同步机制,需要借助Linux提供其他工具来进行同步,通常使用信号灯。

使用共享内存的步骤:

1.调用shmget()创建共享内存段id,

2.调用shmat()将id标识的共享内存段加到进程的虚拟地址空间,

3.访问加入到进程的那部分映射后地址空间,可用IO操作读写。

常用API如下:

函数

int shmget(key_t key, size_t size, int shmflg)

参数key

由ftok生成的key标识,标识系统的唯一IPC资源

参数size

需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。

参数shmflg

如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用IPC_CREAT或直接传0

返回值

成功时返回一个新建或已经存在的共享内存标识符,取决于shmflg的参数。失败返回-1并设置错误码。

功能

创建共享内存

函数

key_t ftok(const char *pathname, int proj_id)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

参数const char *pathname

文件路径以及文件名

参数int proj_id

字符

返回值

成功返回key值,失败返回-1

功能

建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

函数

void *shmat(int shmid, const void *shmaddr, int shmflg)

头文件

#include <sys/types.h>

#include <sys/shm.h>

参数int shmid

共享内存的标识符,也就是shmget函数的返回值

参数const void *shmaddr

映射到的地址,一般写NULL,NULL为系统自动帮我完成映射

参数int shmflg

通常为0,表示共享内存可读可写,或者为SHM_RDONLY,表示共享内存可读可写

返回值

成功返回共享内存映射到进程中的地址,失败返回-1

功能

挂接共享内存

函数

int shmdt(const void *shmaddr)

头文件

#include <sys/types.h>

#include <sys/shm.h>

参数const void *shmaddr

共享内存映射后的地址

返回值

成功返回0,失败返回-1

功能

去关联共享内存

注意

shmdt函数是将进程中的地址映射删除,也就是说当一个进程不需要共享内存的时候,就可以使用这个函数将他从进程地址空间中脱离,并不会删除内核里面的共享内存对象。

函数

int shmctl(int shmid, int cmd, struct shmid_ds *buf)

头文件

#include <sys/ipc.h>

#include <sys/shm.h>

参数int shmid

要删除的共享内存的标识符

参数int cmd

IPC_STAT (获取对象属性) IPC_SET  (设置对象属性) IPC_RMID(删除对象)

参数struct shmid_ds *buf

指定IPC_STAT (获取对象属性) IPC_SET  (设置对象属性) 时用来保存或者设置的属性

功能

销毁共享内存

实验代码:

在程序中,创建共享内存。

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
    int shmid;
    shmid = shmget(IPC_PRIVATE, 1024, 0777);
    if (shmid < 0)
    {
        printf("shmget is error\n");
        return -1;
    }
    printf("shmget is ok and shmid is %d\n", shmid);
    return 0;
}

编译运行程序如下图所示: 

输入以下命令查看到创建的共享内存段的id和上面程序获取到的共享内存段的id是一样的。

实验代码

在程序中,父子进程通过共享内存通信。

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{

    int shmid;
    key_t key;
    pid_t pid;
    char *s_addr, *p_addr;
    key = ftok("./a.c", 'a');
    shmid = shmget(key, 1024, 0777 | IPC_CREAT);
    if (shmid < 0)
    {
        printf("shmget is error\n");
        return -1;
    }
    printf("shmget is ok and shmid is %d\n", shmid);
    pid = fork();
    if (pid > 0)
    {
        p_addr = shmat(shmid, NULL, 0);
        strncpy(p_addr, "hello", 5);
        wait(NULL);
        exit(0);
    }
    if (pid == 0)
    {
        sleep(2);
        s_addr = shmat(shmid, NULL, 0);
        printf("s_addr is %s\n", s_addr);
        exit(0);
    }
    return 0;
}

 编译运行程序如下图所示:

优点:我们可以看到使用共享内存进行进程之间的通信是非常方便的,而且函数的接口也比较简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。

缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。

35.5 消息队列

本章内容对应视频讲解链接(在线观看):

消息队列  https://www.bilibili.com/video/BV1zV411e7Cy?p=30

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\25”目录下。

System V IPC包含三种进程间通信机制,有消息队列,信号灯(也叫信号量),共享内存。此外还有System V IPC的补充版本POSIX IPC,这两组IPC的通信方法基本一致,本章以System V IPC为例介绍Linux进程通信机制。

可以用命令“ipcs”查看三种IPC,“ipcrm”删除IPC对象。比如在i.MX6ULL开发板终端输入“ipcs”查看系统中存在的IPC信息:

这些 IPC对象存在于内核空间,应用层使用IPC通信的步骤为:

 

1. 获取 key值,内核会将 key值映射成IPC标识符,获取key值常用方法:

  1. 在get调用中将IPC_PRIVATE常量作为key值。
  2. 使用ftok()生成key。

2. 执行IPC get调用,通过key获取整数IPC标识符id,每个id表示一个IPC对象。

接口

消息队列

共享内存

信号灯

创建/打开对象

msgget()

shmget()

semget()

3. 通过id访问IPC对象。

接口

消息队列

共享内存

信号灯

读写/调整

msgsnd()/msgrcv()

shmat()

semop()

4. 通过id控制IPC对象

接口

消息队列

共享内存

信号灯

控制

msgctl()

shmctl()

semctl()

创建这三种IPC对象都要先获取key值,然后根据key获取id,用到的函数如下:

函数

key_t ftok(const char *pathname, int proj_id)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

参数pathname

路径名或文件名

参数proj_id

同一个文件根据此值生成多个key值,int型或字符型,多个若想访问同一IPC对象,此值必须相同。

返回值

成功返回key值,失败返回-1

功能

建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

下面介绍消息队列:

消息队列是类 unix系统中一种数据传输的机制,其他操作系统中也实现了这种机制,可见这种通信机制在操作系统中有重要地位。

Linux内核为每个消息队列对象维护一个msqid_ds,每个msqid_ds对应一个id,消息以链表形式存储,并且msqid_ds存放着这个链表的信息。

 

消息队列的特点:

1.发出的消息以链表形式存储,相当于一个列表,进程可以根据id向对应的“列表”增加和获取消息。

2.进程接收数据时可以按照类型从队列中获取数据。

消息队列的使用步骤:

     1. 创建key;

     2. msgget()通过key创建(或打开)消息队列对象id;

    3. 使用msgsnd()/msgrcv()进行收发;

    4. 通过msgctl()删除ipc对象

通过msgget()调用获取到id后即可使用消息队列访问IPC对象,消息队列常用API如下:

函数

int msgget(key_t key, int msgflg)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

参数key_t key

和消息队列相关的key值

参数int msgflg

访问权限

返回值

成功返回消息队列的ID,失败-1

功能

获取IPC对象唯一标识id

函数

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

参数int msqid

消息队列ID

参数const void *msgp

指向消息类型的指针

参数size_t msgsz

发送的消息的字节数。

参数int msgflg

如果为0,直到发送完成函数才返回,即阻塞发送

IPC_NOWAIT:消息没有发送完成, 函数也会返回,即非阻塞发送

返回值

成功返回0,失败返回-1

功能

发送数据

函数

int msgctl(int msqid, int cmd, struct msqid_ds *buf)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

参数int msqid

消息队列的ID

参数int cmd

IPC_STAT:读取消息队列的属性,然后把它保存在buf指向的缓冲区。

IPC_SET:设置消息队列的属性,这个值取自buf参数

IPC_RMID:删除消息队列

参数struct msqid_ds *buf

消息队列的缓冲区

返回值

成功返回0,失败返回-1

功能

控制操作,删除消息队列对象等

函数

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)

参数msqid

IPC对象对应的 id

参数msgp

消息指针,消息包含类型和字段

参数msgsz

消息里的字段大小

参数msgtyp

消息里的类型

参数msgflg

位掩码,不止一个

返回值

成功返回接收到的字段大小,错误返回-1

功能

接收消息

实验代码 

a.c向消息队列里面写 

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf
{
    long mtype;
    char mtext[128];
};
int main(void)
{
    int msgid;
    key_t key;
    struct msgbuf msg;
         //获取key值
    key = ftok("./a.c", 'a');
    //获取到id后即可使用消息队列访问IPC对象
    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid < 0)
    {
        printf("msgget is error\n");
        return -1;
    }
    printf("msgget is ok and msgid is %d \n", msgid);
    msg.mtype = 1;
    strncpy(msg.mtext, "hello", 5);
   //发送数据
    msgsnd(msgid, &msg, strlen(msg.mtext), 0);
    return 0;
}

b.c从消息队列里面读

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
    long mtype;
    char mtext[128];
};
int main(void)
{
    int msgid;
    key_t key;
    struct msgbuf msg;
    key = ftok("./a.c", 'a');
    //获取到id后即可使用消息队列访问IPC对象
    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid < 0)
    {
        printf("msgget is error\n");
        return -1;
    }
    printf("msgget is ok and msgid is %d \n", msgid);
    //接收数据
    msgrcv(msgid, (void *)&msg, 128, 0, 0);
    printf("msg.mtype is %ld \n", msg.mtype);
    printf("msg.mtext is %s \n", msg.mtext);
    return 0;
}

在Ubuntu上开一个终端,编译运行a如下图所示:

输入以下命令查看如下图所示: 

 

在Ubuntu再开一个终端,编译运行b如下图所示,成功从消息队列里面读到信息。 

35.6 信号

本章内容对应视频讲解链接(在线观看):

信号量  信号量_哔哩哔哩_bilibili

代码在配套资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\1.系统编程例程\系统编程配套程序\linux\26”目录下。

本章节将讲述另一种进程间通信的机制——信号量。注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物。为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来协调进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

信号灯也叫信号量,它能够用来同步进程的动作,不能传输数据。它的应用场景就像红绿灯,控制各进程使用共享资源的顺序。Posix无名信号灯用于线程同步, Posix有名信号灯,System V 信号灯。信号灯相当于一个值大于或等于0计数器,信号灯值大于0,进程就可以申请资源,信号灯值-1,如果信号灯值为0,一个进程还想对它进行-1,那么这个进程就会阻塞,直到信号灯值大于1。

使用System V信号灯的步骤如下:

  1. 使用semget()创建或打开一个信号灯集。
  2. 使用semctl()初始化信号灯集,。
  3. 使用semop()操作信号灯值,即进行 P/V操作。

P操作:申请资源,申请完后信号灯值-1;

V操作:释放资源,释放资源后信号灯值+1;

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

函数

int semget(key_t key, int nsems, int semflg)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

参数key_t key

信号量的键值

参数int nsems

信号量的数量

参数int semflg

标识

返回值

成功返回信号量的ID,失败返回-1

功能

    创建一个新信号量或取得一个已有信号量

函数

int semctl(int semid, int semnum, int cmd, union semun arg)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

参数int semid

信号量ID

参数int semnum

信号量编号

参数cmd

IPC_STAT(获取信号量的属性) IPC_SET(设置信号量的属性)IPC_RMID (删除信号量)  SETVAL(设置信号量的值)

参数arg

 union semun {

                int   val;   

                  struct semid_ds *buf;                   

          unsigned short  *array;  

              struct seminfo  *__buf;    

 }

功能

初始化信号灯集合

函数

int semop(int semid, struct sembuf *sops, size_t nsops)

头文件

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

参数int semid

信号量ID

参数struct sembuf *sops

信号量结构体数组

参数size_t nsops

要操作信号量的数量

struct sembuf{

       unsigned short sem_num; //要操作的信号量的编号             

       short sem_op;  //P/V操作,1为V操作,释放资源。-1为P操作,分配资源。0为等待,直到信号量的值变成0

          short  sem_flg; //0表示阻塞,IPC_NOWAIT表示非阻塞

}

功能

在信号量上执行一个或多个操作。

实验代码:

指定哪个进程运行,可以使用进程间通信的知识,或者使用信号量,这里以使用信号量为例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

union semun
{
    int val;
};
int main(void)
{
    int semid;
    int key;
    pid_t pid;
    struct sembuf sem;
    union semun semun_union;
    key = ftok("./a.c", 0666);
    semid = semget(key, 1, 0666 | IPC_CREAT);
    semun_union.val = 0;
    semctl(semid, 0, SETVAL, semun_union);
    pid = fork();
    if (pid > 0)
    {
        sem.sem_num = 0;
        sem.sem_op = -1;
        sem.sem_flg = 0;
        semop(semid, &sem, 1);
        printf("This is parents\n");
        sem.sem_num = 0;
        sem.sem_op = 1;
        sem.sem_flg = 0;
        semop(semid, &sem, 1);
    }
    if (pid == 0)
    {
        sleep(2);
        sem.sem_num = 0;
        sem.sem_op = 1;
        sem.sem_flg = 0;
        semop(semid, &sem, 1);
        printf("This is son\n");
    }
    return 0;
}

编译运行程序如下图所示:

   信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。 

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值