Linux学习笔记(三)----进程间通信

目录

进程间通信(IPC)的介绍

一、管道

1.1管道的特点:

1.2原型: 

1.3例子:

二、命名管道

2.1命名管道的特点:        

2.2原型: 

2.3例子:

三、消息队列

3.1 消息队列的特点:

3.2 原型:  

3.3 例子: 

四、共享内存

4.1内存映射机制(mmap):

4.2共享内存的相关概念

4.3相关函数原型

4.4例子

五、信号

5.1信号的相关概念

5.2信号的处理

5.3信号的相关函数原型

六、信号量

6.1信号量的相关概念

6.2信号量的工作原理        

6.3相关的API

6.4例子


进程间通信(IPC)的介绍

        进程间通信方式一般有以下几种:
                1、管道,匿名管道,命名管道
                2、消息队列
                3、共享内存
                4、信号
                5、信号量
                6、socket

        每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。

一、管道

        管道,通常是指匿名管道,是UNIX中IPC最古老的形式。

1.1管道的特点:

  •  管道数据只能单向流动,所以如果要实现双向通信,就要创建2个管道
  • 管道分为匿名管道和命名管道
    • 匿名管道只能在父子进程关系之间使用
    • 命名管道,可以在不关联的两个进程之间使用,因为它创建了一个类型为管道的设备文件,使用这个设备文件就可以通信。
  • 管道只能承载无格式的字节流
  • 管道可以看成是一种特殊的文件,对于他的读写也可以使用普通的read,write等函数,但他不是普通的文件,并不属于其它任何文件系统,只存在于内存中。           

1.2原型: 

#include <unistd.h>
int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

        管道建立会产生两个文件描述符,fd[0]为读而打开,fd[1]为写而打开。

 可以通过close()函数关闭管道

1.3例子:

        此例子为:创建一个匿名管道,父进程往管道里读数据,子进程从管道写数据

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

int main()
{

    int fd[2];
    
    pid_t pid;

    int ret;
    int nread;
    char readbuf[24] = {'\0'};
    char writebuf[24] = "Hello, world!";

    ret = pipe(fd);                                  //创建管道

    if(ret == -1){
        printf("creat pipe failed!\n");
        return -1;
    }

    pid = fork();                                     // 创建子进程

    if(pid < 0){
        printf("creat failed!\n");
        return -1;
    }
    
    else if(pid > 0){                               //父进程从管道中读数据
        close(fd[1]);                               //关闭写的一端
        nread = read(fd[0], readbuf, 24);           //当nread等于0时read会阻塞在这,直到 
                                                    //               readbuf中有数据到来
        if(nread == -1){    
            printf("read failed!\n");
            return -1;
        }
        printf("from child data: %s", readbuf);
    
    }

    else{                                          //子进程往管道中写数据
        close(fd[0]);                              //关闭读的一端
        ret = write(fd[1], writebuf, strlen(writebuf));
        if(ret == -1){
            printf("write failed!\n");
            return -1;
        }
        exit(0);
    }
    return 0;
}

二、命名管道

        命名管道(FIFO),与匿名管道不同,它是一种文件类型

2.1命名管道的特点:        

  1. FIFO可以在无关的进程之间交换数据,与无名管道不同。

  2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2.2原型: 

#include <sys/stat.h>     
int mkfifo(const char *pathname, mode_t mode);    // 返回值:成功返回0,出错返回-1

参数:pathname 文件名

     mode:该mode与open函数的mode 相同,一旦创建了fifo,就可以用一般的文件IO函数操作它

         当open一个FIFO是,是否设置非阻塞标志的区别:
        默认设置阻塞(O_NONBLOCK), 只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打开次FIFO。
        若没有设置阻塞,则只读open立即返回。而只写open将出错返回-1,此情况很少用,一般默认即可。

2.3例子:

        创建一个命名管道(FIFO),一个程序往里面写数据,一个程序从里面读数据

//readfifo.c

#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int fd;
    int nread;

    char readbuf[128] = {'\0'};
                                                    //创建管道
    if( (mkfifo("./test", 0600) == -1) && errno != EEXIST){
            printf("mkfifo successful!\n");
    }

    fd = open("./test", O_RDONLY);           //open要阻塞到某个其他进程为写而打开此FIFO
                                                 
    if(fd == -1){
        printf("open failed!\n");
        return -1;
    }

    nread = read(fd, readbuf, 128);         //将读到的数据放到readbuf中,大小可根据需求调节

    if(nread == -1){
        printf("read failed!\n");
        return -1;
    }
                
    printf("from fifo %d: byte\tcontext:%s\n", nread, readbuf);

    close(fd);                                  //关闭文件
    return 0;
}

此程序为:对管道写入数据

// fifowrite.c

#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int fd;
    int nwrite;

    char writebuf[128] = "Hello, world!";

    fd = open("./test", O_WRONLY);                        //打开fifo
    if(fd == -1){
        printf("open failed!\n");
        return -1;
    }
    nwrite = write(fd, writebuf, strlen(writebuf));        //写数据到fifo

    if(nwrite == -1){
        printf("write failed!\n");
        return -1;
    }
    close(fd);                                             //关闭文件

    return 0;
}

三、消息队列

        消息队列是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来表识

         知道了消息队列,了解一下消息缓冲区 上图中绿色圈的部分就是缓冲区,用来存放通道号,和写入通道中的数据。

struct msgbuf{
	long channel;  //通道号,必须大于0(系统内部其实将它定义为 mtype ——把它视为一个类型)
	char mtext[100];//消息内容(100是自定义的值,你可以任意更改)
	}:

        通道的概念:通道号就相当于一个分类,并不真实存在。channel 值相同的属于同一通道,系统可以根据通道号来选择对应的消息队列。 

3.1 消息队列的特点:

        1.消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级

        2.消息队列独立于发送和接收的过程。进程结束时,消息队列及其内容并不会被删除。(因为消息队列存放在内核中,由内核统一管理)

        3.消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序查询,也可以按消息的类型读取。(链表的特性)

3.2 原型:  

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


//创建或打开消息队列: 成功返回队列ID, 失败返回-1;
int msgget(key_t key, int flag)          // 参数key, 可通过ftok函数获得。 
                     //flag:
                    //IPC_CREAT——创建一个新的消息队列

                    //IPC_EXCL——表示创建的消息队列存在,返回错误(与IPC_CREATE一同使用)

                    //0 ——是已存在的消息队列,直接打开

                    //IPC_NOWAIT——读写消息队列无法满足时,不阻塞。(是msgsnd 和msgrcv 的权限)


//添加消息
int msgsnd(int msqid, const void *ptr, size_t size, int flag);

//misgid——msgget函数的返回值,想写入内容的消息队列的标识符id
//ptr——临时创建一个消息队列结构体对象的指针
//size——写入消息的大小,char metex[100]的大小,不包括通道号
//flag——权限(0——阻塞方式,当输入的通道号不合法,会一直阻塞,不会结束,除非用Ctrl C
//              IPC_NOWAIT——非阻塞方式,当输入的通道号不合法,不会阻塞,直接结束。



//读取消息
int msgrcv(int msqid, const void *ptr, size_t size, long type, int flag);

//控制消息队列: 成功返回0, 失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);     

 在看原型函数之前先介绍一个函数ftok()函数。

key_t ftok(const char*filename,proj_id)
第一个参数:
//filename是一个文件名(路径),要求这个文件名必须存在——(一般用const修饰)
//一般默认为当前文件
例:key_t key;
key=ftok(".",1)——将当前目录设为你的文件名

第二个参数:是子序号,虽然是int型,但是只使用8 bits,(1~255)

返回值:-1(失败)   (成功)key_t 的结果

3.3 例子: 

看过ftok()函数后就可以配合msgget()函数使用了

代码实现:

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


int main()
{
    key_t key;
    int msgId;

    key = ftok(".", 1);
    if(key == -1){
        perror("ftok");
        return -1;
    }

    msgId = msgget(key, IPC_CREAT|IPC_EXCL|0600);    //创建消息队列,若已存在该消息队列则报错
    if(msgId == -1){
        perror("msgget");
        return -1;
    }
    
    printf("msgget successful!\n");

    return 0;
}

在上个例子的基础上看一个往消息队列中写数据的例子:

再看例子之前要先搞清写数据和消息队列两者的关系

1.先将想写入的消息内容存放在 消息队列结构体的对象中,这个对象相当于一个中转站;

2.再将中转站里的内容放入到 miqId对应的消息队列中;

3.从而间接的实现 往消息队列中写数据。

代码实现:

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

struct msgbuf{
	long channel;                          //通道号,
	char mtext[100];                       //消息内容(100是自定义的值,你可以任意更改)
};    

int main()
{
    key_t key;
    int msgId;
    int ret;

    struct msgbuf tmsg;
    memset(&tmsg, 0, sizeof(tmsg));          //初始化,否则因为缓冲区的问题,会出现错误

    key = ftok(".", 1);
    if(key == -1){
        perror("ftok");
        return -1;
    }

    msgId = msgget(key, IPC_CREAT|0600);        //创建消息队列
    if(msgId == -1){
        perror("msgget");
        return -1;
    }

    tmsg.channel = 777;                        //可以自定义值
    tmsg.mtext = "Hello, world!";              //将数据写入结构体对象,也就是消息缓冲区

    ret = msgsnd(msgId, &tmsg, strlen(tmsg.mtext), 0);

    if(ret == -1){
        perror("msgsnd");

    if(msgctl(msgId, IPC_RMID, 0) == -1){        //删除队列,下面对此函数有介绍
        perror("msgctl");                        
        return -1;
    }

    return 0;

}

写入数据后,我们可以从消息队列中读取数据

        我们还是要先搞清读数据和消息队列两者的关系

1.先将msgqid对应的消息队列的内容放在这个中转站(临时创建的结构体对象)中;

2.再对中转站里的内容进行读取;

3.从而间接的实现 从消息队列中读数据。

代码实现:

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

struct msgbuf{
	long channel;  //通道号,
	char mtext[100];//消息内容(100是自定义的值,你可以任意更改)
};


int main()
{
    key_t key;
    int msgId;
    int nread;

    struct msgbuf tmsg;
    memset(&tmsg, '\0', sizeof(tmsg));          //初始化,否则因为缓冲区的问题,会出现错误

    key = ftok(".", 1);
    if(key == -1){
        perror("ftok");
        return -1;
    }

    msgId = msgget(key, 0);        //打开消息队列
    if(msgId == -1){
        perror("msgget");
        return -1;
    }

    tmsg.channel = 777;                    //可以自定义值,但两者的channel相同才可以通信
    tmsg.mtext = {'\0'};             

 
    nread = msgrcv(msgId, &tmsg, sizeof(tmsg.mtext), tmsg.channel, 0);

    if(nread == -1){
        perror("msgrcv");

    printf("from msgSnd %d\t %s\n", nread, tmsg.mtext);


    return 0;

}



最后就是删除消息队列

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
 //返回值:成功返回0,失败返回-1
 
//msqid 消息队列标识符id,mspid是msgget的返回值
//cmd:包括以下三种 
  1. IPC_STAT
	读取消息队列的数据结构msqid_ds,并将其存储在buf指定的地址中。
  2.IPC_SET
	设置消息队列的数据结构msqid_ds中的ipc_perm元素的值。这个值取自buf参数。
  3.IPC_RMID
	从系统内核中移走消息队列,即删除消息队列
	(buf 配合IPC_RMID使用,设为0,即buf为NULL)

// buf 通常设置为0

代码实现:

就是在上面的两个代码中加入msgctl()API即可。下面具一个简洁的例子

int main()
{

    int id=msgget(111,IPC_CREAT|0600);        //创建队列
    if(id == -1)
        perror("msgget"),exit(1);
    printf("create success\n");
    if(msgctl(id,IPC_RMID,0) == -1)            //删除队列
        perror("msgtcl");
        exit(1);
    printf("msgctl success\n");
    return 0;
}

四、共享内存

在了解共享内存之前先了解一下内存映射机制(mmap)的概念。

4.1内存映射机制(mmap):

        当CPU读取数据时,是由内存管理单元(MMU)管理的。MMU位于CPU与物理内存之间,它包含从虚地址向物理内存地址转化的映射信息。当CPU引用一个内存位置时,MMU决定哪些页需要驻留(通常通过移位或屏蔽地址的某些位)以及转化虚拟页号到物理页号。

        当某个进程读取磁盘上的数据时,进程要求其缓冲通过read()系统调用填满,这个系统调用导致内核想磁盘控制硬件发出一条命令要从磁盘获取数据。磁盘控制器通过DMA直接将数据写入内核的内存缓冲区,不需要CPU协助。当请求read()操作时,一旦磁盘控制器完成了缓存的填写,内核从内核空间的临时缓存拷贝数据到进程指定的缓存中。

        用户空间是常规进程所在的区域,该区域执行的代码不能直接访问硬件设备。内核空间是操作系统所在的区域,该区域可以与设备控制器通讯,控制用户区域进程的运行状态。

        使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O 操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

4.2共享内存的相关概念

        共享内存就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。

        不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

注意:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,如信号量,在下文我们对其有所介绍。

        在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。


我们可以通过查看相关的指令对系统中的共享内存进行查看和操作

         ipcs -m                        查看系统中的共享内存段

        ipcrm -m [shmId]        删除系统中的共享内存段, shmId为共享内存标识符

还有  ipcs -a                        查看所有  可自行查阅

        ipcs -s                         查看系统中的信号量

        ipcrm -s semId                删除系统中的信号量

        ipcrm -q msgId                 删除系统中的消息队列

4.3相关函数原型

        1.创建共享内存

int shmget(key_t key, size_t size, int flag);

//key通过ftok()函数产生,上文消息队列中有介绍
// size: 需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,
//为了避免内存碎片,我们一般申请的内存大小为页的整数倍(通常以兆为单位)。

// flag 和上文消息队列的创建参数flag一样

返回值:成功时返回一个新建或已经存在的的共享内存标识符,失败返回-1并设置错误码

        2.挂接共享内存

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

//shmid, 共享存储标识符

//shmaddr,通常设置为NULL,(存储段连接到由内核选择的第一个可以地址上)

//shmflg,一般为0    //可自行查阅相关参数

//返回值:如果成功,返回共享存储段地址,出错返回-1

        3.关联共享内存

int shmdt(void *addr);

//addr:共享存储段的地址,以前调用shmat时的返回值
//返回值:成功返回0,失败返回-1;

函数功能:当进程不需要共享内存的时候,就需要去关联。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。

        4.销毁共享内存

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//shmid:共享内存标识符

//cmd: 与销毁消息队列中的cmd相同
//      有IPC_STAT,IPC_RMID,SHM_LOCK,SHM_UNLOCK 具体可参考上文       


//buf一般设置为NULL

//返回值:成功返回0,失败返回-1。

注意:共享内存不会随着程序结束而自动消除,要么调用shmctl销毁,要么自己用手敲命令去删除,否则永远留在系统中。

4.4例子

        下面演示一个完整的共享内存的使用例子:

// shmSend.c

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


int main()
{
    int shmId;
    key_t key;
    char *shmaddr;
    int tmp;


    key = ftok(".", 1);                                //获取key值
    if(key == -1){
        perror("key");
        exit(-1);
    }

    shmId = shmget(key, 1024*4, IPC_CREAT|IPC_EXCL|0644);        //创建共享内存
    if(shmId == -1){
        perror("shmget");
        exit(-1);
    }

    shmaddr = shmat(shmId, NULL, 0);                            //挂接共享内存

    strcpy(shmaddr, "Hello, This is Send Server!");             //往共享内存中写数据
    sleep(5);                                              //休眠5秒,等待其他进程读取数据

    tmp = shmdt(shmaddr);                                       //关连共享内存
    if(tmp == -1){
        perror("shmdt");
        exit(-1);
    }

    shmctl(shmId, IPC_RMID, NULL);                             //销毁共享内存

    return 0;
}

从共享内存中获取数据:

// shmGet.c

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


int main()
{
    int shmId;
    key_t key;
    char *shmaddr;
    int tmp;


    key = ftok(".", 1);
    if(key == -1){
        perror("key");
        exit(-1);
    }

    shmId = shmget(key, 1024*4, 0);                        //打开共享内存
    if(shmId == -1){
        perror("shmget");
        exit(-1);
    }
    shmaddr = shmat(shmId, NULL, 0);                       //挂接共享内存

    printf("from sendserver: %s\n", shmaddr);              //从共享内存中读取数据
    
    tmp = shmdt(shmaddr);                                //关连共享内存

    if(tmp == -1){
        perror("shmdt");
        exit(-1);
    }

    return 0;
}

五、信号

5.1信号的相关概念

        信号其实就是一个软件中断。比如:按下Ctrl-C,键盘输入产生一个硬件中断,终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。

        5.1.1信号的种类

        可通过kill -l 来查看

具体的信号采取的动作和详细信息可查看:man 7 signal

SIGHUP:1号信号,Hangup detected on controlling terminal or death of controlling process(在控制终端上挂起信号,或让进程结束),ation:term

SIGINT:2号信号,Interrupt from keyboard(键盘输入中断,ctrl + c ),action:term

SIGQUIT:3号信号,Quit from keyboard(键盘输入退出,ctrl+ | ),action:core,产生core dump文件

SIGABRT:6号信号,Abort signal from abort(3)(非正常终止,double free),action:core

SIGKILL:9号信号,Kill signal(杀死进程信号),action:term,该信号不能被阻塞、忽略、自定义处理
 

        5.1.2信号的产生

                1.硬件产生即通过终端按键产生的信号如:ctrl + c(SIGINT(2))、ctrl + z

                2.软件产生即调用系统函数向进程发信号如:

                        kill()函数,下面有介绍。                                                

                        kill命令:kill -[信号] pid,
                        abort:void abort(void);,收到6号信号,谁调用该函数,谁就收到信号
                        alarm:unsigned int alarm(unsigned int seconds);,收到14号信号,告诉内核在                         seconds秒后给进程发送SIGALRM信号,该信号默认处理动作为终止当前进程。

5.2信号的处理

        进程对信号的处理有三种方式:

                1.忽略信号 SIG_IGN 

                        顾名思义就是系统对其不采取任何操作,但有两个信号不能忽略:SIGKILL(9号信号)和SIGSTOP(19号信号)。

                2.捕获并处理信号

                        即用户自定义的信号处理函数来处理

                3.执行默认操作SIG_DFL

                        默认操作通常是终止进程,这取决于被发送的信号。

5.3信号的相关函数原型

1.signal函数     

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

//signum 捕获的信号值

//handler 函数指针,对捕获的信号进行处理

 举一个调用ctrl + c(SIGINT(2))无法退出程序的例子

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


void handler(int numsignal)
{
    printf("capture signal: %d\n", numsignal);
    printf("NO EXIT!\n");
}

int main()
{
    if(signal(SIGINT, handler) == SIG_ERR){            //捕获信号
        perror("signal");
        exit(-1);
    }

    while(1);                                
    return 0;
}

2.kill函数 

int kill(pid_t pid, int sig);

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

pid:指定进程的进程ID,注意用户的权限,比如普通用户不可以杀死1号进程(init)

上面的例子只是对信号的处理,不能处理带消息的信号。若要处理可采用sigaction();

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

//act 一个struct sigaction的结构体地址


 struct sigaction {
               void     (*sa_handler)(int);    //函数指针,保存了内核对信号的处理方式
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;        //保存的是当进程在处理信号的时候,收到的信号
               int        sa_flags;        
               void     (*sa_restorer)(void);
           };

具体实现:

//send

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

int main(int argc, char** argv)
{
    int signum;
    int pid;

    if(argc != 3){
        printf("input error!\n");
        exit(-1);
    }

    signum = atoi(argv[1]);
    pid = atoi(argv[2]);

    union sigval value;
    value.sival_int = 888;

    sigqueue(pid, signum, value);                //发送数据

    printf("pid = %d\n done!\n", getpid());

    return 0;
}
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>


void handler(int numsignal, siginfo_t *info, void *context)
{
    printf("capture signal: %d\n", numsignal);

    if(context != NULL){

        printf("get data:%d\n", info->si_value.sival_int);
        printf("get from %d\n", info->si_pid);
    }
}


int main()
{

    struct sigaction act;

    printf("pid = %d\n", getpid());

    act.sa_handler = handler;                       //对捕获的信号进行处理
    act.sa_flags = SA_SIGINFO;                      //使信号可附带消息

    sigaction(SIGUSR1, &act, NULL);


    while(1);
    return 0;
}

六、信号量

6.1信号量的相关概念

        信号量与IPC结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,
    不是用于存储进程间通信数据。

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

        临界资源:多道程序系统中存在许多进程,他们共享资源,而很多资源一次只能供一个进程使用。一次仅供一个进程的资源称为临界资源。

        在操作系统中,有临界区的概念。临界区内放的一般是被1个以上的进程或线程(以下只说进程)共用的数据。

        临界区内的数据一次只能同时被一个进程使用,当一个进程使用临界区内的数据时,其他需要使用临界区数据的进程进入等待状态。

6.2信号量的工作原理        

        信号量可以进行两种操作:等待和发送信号,即P(sv)和V(sv),它们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

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

6.3相关的API

        Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量

 1.semget函数

       semget创建一个新信号量或取得一个已有信号量,原型为:

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


int semget(key_t key, int num_sems, int sem_flags);

// num_sems :指定需要的信号量数目,它的值一般为1。如果是引有一个现有的集合,该值设置为0.

// flag, 与上文信号量,共享内存的相关创建api中的flag相同
//           如IPC_CREAT | IPC_EXCL | 0600

返回值: 成功返回一个相应信号标识符(非零),失败返回-1.

2.semop函数

        semop的作用是改变信号量的值,原型为:

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);

//sem_id  semget函数的返回的信号量标识符


//第二个参数的结构定义

//struct sembuf{
    short sem_num;        //除非使用一组信号量,否则它为0
    short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                    //一个是+1,即V(发送信号)操作。
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                    //并在进程没有释放该信号量而终止时,操作系统释放信号量
};

3.semctl函数

        semctl直接控制信号量信息,它的原型为:

int semctl(int sem_id, int sem_num, int command, ...);

// command:如果是想要删除一个已经无需继续使用的信号量标识符则用 IPC_RMID与上文中的消息队列销毁类似
//        如果是用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用
//    是在信号量第一次使用前对它进行设置(和第四个参数配合使用)。用 SETVAL 

第四个参数配合SETVAL使用 作用是对信号量初始化。

union semun{
    int val;                    //一般对其进行修改,可理解为信号量的初始值设置为0,为1都可
    struct semid_ds *buf;
    unsigned short *arry;
};

6.4例子

        来举一个信号量配合消息队列使用的例子,就对上文中的消息队列进行改进

//send

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


struct msgbuf {
        long channel;
        char mtext[100];
};

union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
};

void pHandle(int semId){                    //P操作
    struct sembuf set;

    set.sem_num = 0;
    set.sem_op = -1;
    set.sem_flg = SEM_UNDO;
   if(semop(semId, &set, 1) == -1){
        perror("psemop");
        exit(-1);
    }
    return;
}

void vHandle(int semId){                 //V操作
    struct sembuf vset;

    vset.sem_num = 0;
    vset.sem_op = 1;
    vset.sem_flg = SEM_UNDO;
    if(semop(semId, &vset, 1) == -1){
        perror("vsemop");
        exit(-1);
    }
    return;
}


int main()
{
    key_t key;
    int msgId;
    int semId;
  int ret;

    struct msgbuf tmsg;
    memset(&tmsg, '\0', sizeof(tmsg));                  //初始化,否则因为缓冲区的问题,会出现错误

    key = ftok(".", 1);
    if(key == -1){
        perror("ftok");
        return -1;
    }

    semId = semget(key, 1, IPC_CREAT | IPC_EXCL |0644);     //创建信号量
    if(semId == -1){
        perror("semId");
        exit(-1);
    }

    union semun initsem;
    initsem.val = 1;
    semctl(semId, 0, SETVAL, initsem);                      //对信号量进行初始化


    msgId = msgget(key, IPC_CREAT | IPC_EXCL | 0600);               //创建消息队列
    if(msgId == -1){
        perror("msgget");
        return -1;
    }
    pHandle(semId);         //在对消息队列写入之前,进行P操作使其它进程不能对消息队列进行访


    tmsg.channel = 777;
    strcpy(tmsg.mtext, "Hello, this is sendserver!");

    ret = msgsnd(msgId, &tmsg, strlen(tmsg.mtext), 0);          //往消息队列中写数据

    if(ret == -1){
        perror("msgsnd");
        return -1;
    }
    vHandle(semId);

    sleep(5);

    if(msgctl(msgId, IPC_RMID, 0) == -1){
        perror("msgctl");
        return -1;
    }

    if(semctl(semId, 0 , IPC_RMID) == -1){
        perror("semctlrm");
        exit(-1);
    }
    return 0;
}
//get

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


struct msgbuf {
        long channel;
        char mtext[100];
};

union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
};

void pHandle(int semId){                    //P操作
    struct sembuf set;

    set.sem_num = 0;
    set.sem_op = -1;
    set.sem_flg = SEM_UNDO;
   if(semop(semId, &set, 1) == -1){
        perror("psemop");
        exit(-1);
    }
    return;
}

void vHandle(int semId){                 //V操作
    struct sembuf vset;

    vset.sem_num = 0;
    vset.sem_op = 1;
    vset.sem_flg = SEM_UNDO;
    if(semop(semId, &vset, 1) == -1){
        perror("vsemop");
        exit(-1);
    }
    return;
}


int main()
{
    key_t key;
    int msgId;
    int semId;
    int ret;
    int nread;

    struct msgbuf tmsg;
    memset(&tmsg, '\0', sizeof(tmsg));                  //初始化,否则因为缓冲区的问题,会出现错误

    key = ftok(".", 1);
    if(key == -1){
        perror("ftok");
        return -1;
    }

    semId = semget(key, 1, 0);      //打开信号量
    if(semId == -1){
        perror("semId");
        exit(-1);
    }

    union semun initsem;
    initsem.val = 1;
    semctl(semId, 0, SETVAL, initsem);                      //对信号量进行初始化


    msgId = msgget(key, 0);             //打开消息队列
    if(msgId == -1){
        perror("msgget");
     return -1;
    }


    tmsg.channel = 777;

    pHandle(semId);                     //p操作进行拿锁

    nread = msgrcv(msgId, &tmsg, sizeof(tmsg.mtext), tmsg.channel, 0);

    if(nread == -1){
        perror("msgrcv");
        return -1;
    }

    printf("from msgSnd %d\t %s\n", nread, tmsg.mtext);

    vHandle(semId);


    return 0;
}

        以上就是关于进程间通信的内容。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值