Linux系统编程——进程间通信

目录

一、进程间通信介绍

二、无名管道通信

2.1 无名管道的特点:

2.2 无名管道的创建与关闭:

2.3 创建无名管道pipe函数原型和头文件:

2.4 无名管道应用:

二、有名管道FIFO

2.1 FIFO的特点:

2.2 FIFO创建函数mkfifo函数原型和头文件:

2.3 使用mkfifo函数创建一个有名管道:

2.4 使用mkfifo函数创建一个管道实现两个进程之间的通信:

三、消息队列

3.1 消息队列的特点:

3.2 消息队列 创建/打开函数msgget()原型和头文件:

3.3 消息队列 发送消息函数msgsnd()原型和头文件:

3.4 消息队列 接收消息函数msgrcv()原型和头文件:

3.5 控制消息队列函数msgctl()原型和头文件:

3.6 获取消息队列函数ftok()原型和头文件:

3.7 使用消息队列实现两个进程之间的通信:

四、共享内存

4.1 共享内存的特点:

4.2 用指令来查看和释放已经存在的共享内存:

4.3 共享内存创建/获取函数shmget()原型和头文件:

4.4 共享内存映射函数shmat()原型和头文件:

4.5 断开与共享内存的连接函数shmdt()原型和头文件:

4.6 共享内存控制/删除函数shmctl()原型和头文件:

4.7 使用共享内存实现两个进程之间的通信:

五、信号

5.1 信号的名称和编号:

5.2 信号的处理:

5.3 信号的应用之杀死进程:

5.4 信号处理函数的注册和信号处理发送函数:

5.5 信号注册函数——入门版

5.5.1 信号注册函数signal()原型和头文件:

5.5.2 捕捉信号:

5.5.4 忽略信号(SIG_IGN):

5.6 信号发送函数——入门版

5.6.1 信号发送函数kill()原型和头文件:

5.6.2 使用信号发送函数kill()实现信号的发送:

5.6.3 使用sprintf和system完成与kill一样的功能:

5.7 信号注册函数——高级版

5.7.1 信号接收函数sigaction()原型和头文件:

5.8 信号发送函数——高级版

5.8.1 信号发送函数sigqueue()原型和头文件:

5.9 使用信号接收函数sigaction()和信号发送函数sigqueue()实现两个进程的通信:

六、信号量

6.1 信号量相关定义:

6.2 信号量特点:

6.3 创建/获取一个信号量组函数semget()原型和头文件:

6.4 控制信号量函数semctl()原型和头文件:

6.5 操作信号量组函数semop()原型和头文件:

6.6 使用信号量来控制父子进程的执行顺序问题:

6.7 使用信号量来控制共享内存读写顺序问题:

一、进程间通信介绍

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息

进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。那么释放的资源可能是其他进程需要的,然而进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

二、无名管道通信

  • 无名管道,是 UNIX 系统IPC最古老的形式

2.1 无名管道的特点:

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定读端和写端

  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的readwrite 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中

2.2 无名管道的创建与关闭:

  • 无名管道是基于文件描述符的通信方式。当一个管道创建时,它会创建两个文件描述符:fd[0]fd[1] 。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道,如下图,这样就构成了一个单向的数据通道:

  • 管道关闭时只需要用 close() 函数将这两个文件描述符关闭即可

2.3 创建无名管道pipe函数原型和头文件:

/*
	Linux下man 2 pipe查看头文件
*/ 
#include <unistd.h>

int pipe(int fd[2]);

int 		函数返回值,管道创建成功返回0,失败则返回-1
int fd[2]	包含两个元素的整型数组,存放管道对应的文件描述符,fd[0]为读而打开,fd[1]为写而打开

2.4 无名管道应用:

单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

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

int main()
{
    int fd[2];
    int fp;
    int pid;
    char *writeBuf = "hello from father";
    char readBuf[128];

    //int pipe(int pipefd[2]);
    fp = pipe(fd);                                      //创建一个管道
    if(fp == -1){
        printf("创建管道失败\n");
    }

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

    if(pid < 0){
        printf("创建子进程失败\n");
    }else if(pid > 0){
        printf("进入父进程\n");
        close(fd[0]);                                   //父进程关闭管道读端
        write(fd[1],writeBuf,strlen(writeBuf));         //父进程将writeBuf的内容写到管道里
        wait(NULL);                                     //父进程等待子进程退出
    }else{
        printf("进入子进程\n");
        close(fd[1]);                                   //子进程关闭管道写端
        read(fd[0],readBuf,sizeof(readBuf));            //子进程把管道中的数据读到readBuf里面
        printf("子进程读取父进程的数据是:%s\n",readBuf);
        exit(0);                                        //子进程退出
    }
    return 0;
}

二、有名管道FIFO

  • FIFO( First Input First Output)简单说就是指先进先出,也称为命名管道,它是一种文件类型

2.1 FIFO的特点:

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

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

2.2 FIFO创建函数mkfifo函数原型和头文件:

/*
	Linux下 man 3 mkfifo查看手册
*/

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

int						函数返回值,成功:则返回 0	失败:返回 -1 , 错误原因存于 errno 中
const char *pathname 	创建管道的文件名/文件路径
mode_t mode				权限模式 如:0600就是可读可写模式
1.可读:        r         4
2.可写:        w         2
3.可执行        x         1
0600:6代表4+2(可读可写)

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它

  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO

2.3 使用mkfifo函数创建一个有名管道:

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>

int main()
{
    int ret = mkfifo("./file",0600);    //创建一个命名管道

    if(ret == 0){                       //函数返回值为0代表创建管道成功
        printf("mkfifo创建管道成功\n");
    }
    if(ret == -1){                      //函数返回值为-1代表创建管道失败
        printf("mkfifo创建管道失败\n");  
        perror("why");                  //输出错误原因
    }
    return 0;
}

2.4 使用mkfifo函数创建一个管道实现两个进程之间的通信:

  • FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。

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

int main()
{
    int i;
    int n_write = 0;
    char *str = "chenlichen handsome";

    //int mkfifo(const char *pathname, mode_t mode);
    if( (mkfifo("./file",0600) == -1) && errno != EEXIST){          //创建一个有名管道
        printf("创建管道失败\n");
        perror("why");
    }

    int fd = open("./file",O_WRONLY);                               //只读方式打开file管道文件
    printf("write open 成功\n");

    for(i=0; i<5; i++){                                             
        sleep(1);                                                   //一秒钟向管道发一次数据
        n_write = write(fd,str,strlen(str));                        //把str中的内容写入到管道中
        printf("向管道写入了%d个字节,写入的内容是:%s\n",n_write,str);
    }
   
    close(fd);                                                      //关闭管道file文件
    return 0;
}
/*read.c*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int i;
    int n_read = 0;
    char readBuf[128] = {0};

    //int mkfifo(const char *pathname, mode_t mode);
    if( (mkfifo("./file",0600) == -1) && errno != EEXIST){                  //创建一个命名管道(如果管道创建失败并且管道文件不存在代表管道创建失败)
        printf("创建管道失败\n");
        perror("why");
    }

    int fd = open("./file",O_RDONLY);                                       //只读方式打开file管道文件
    printf("open打开管道成功\n");

    for(i=0; i<5; i++){
        n_read = read(fd,readBuf,sizeof(readBuf));                          //将管道中的数据读取到readBuf中
        printf("从管道中读了%d个字节,读出的内容是:%s\n",n_read,readBuf);
    }
    
    close(fd);                                                              //关闭管道文件
    return 0;
}

先运行read,令其阻塞,再运行write,向管道写入信息。

三、消息队列

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

3.1 消息队列的特点:

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

  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

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

3.2 消息队列 创建/打开函数msgget()原型和头文件:

/*
	Linux下 man 2 msgget查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);	//

int			函数返回值,成功:返回消息队列的ID	  出错:-1,错误原因存于error中
key_t key	函数ftok的返回值(ID号)或IPC_PRIVATE
    
int msgflg	
1. IPC_CREAT:创建新的消息队列。
2. IPC_EXCL:与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。 
3. IPC_NOWAIT:读写消息队列要求无法满足时,不阻塞。返回值: 调用成功返回队列标识符,否则返回-1.

/*	函数说明:用于创建一个新的或打开一个已经存在的消息队列,此消息队列与key相对应	*/    

3.3 消息队列 发送消息函数msgsnd()原型和头文件:

/*
	Linux下 man 2 msgsnd查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

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

int				函数返回值,成功返回0,失败返回-1 错误原因存于error中
int msqid		由msgget函数返回的消息队列标识码,表示往哪个消息队列发数据
    
void *msgp		发送给队列的消息。是⼀个指针,指针指向准备发送的消息(即准备发送的消息的内容)msgp定义的参照格式如下:
    
                struct msgbuf {
                        long mtype;       /* message type, must be > 0 */
                        char mtext[128];  /* message data */
                };
                long mtype		:它必须以⼀个long int⻓整数开始,接收者函数将利⽤这个⻓整数确定消息的类型
                char mtext[128]	:保存消息内容的数组或指针,它必须小于系统规定的上限值

size_t msgsz	要发送消息的大小,不含消息类型占用的4个字节,即mtext的长度
                    
int msgflg
1. 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
2. IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
3. IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。   
                    
/*	函数说明:将msgp消息写入到标识符为msqid的消息队列	*/    

3.4 消息队列 接收消息函数msgrcv()原型和头文件:

/*
	Linux下 man 2 msgrcv查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

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

ssize_t			函数返回值,成功返回:实际读取到的消息数据长度,失败返回-1,错误原因存于error中
    
int msqid		由msgget函数返回的消息队列标识码,表示从哪个消息队列拿数据
void *msgp		指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构 
size_t msgsz	是msgp指向的消息⻓度,这个⻓度不含保存消息类型的那个long int⻓整型
long msgtyp		接收消息的类型,这个msgtyp和结构体msgbuf内的msgtyp是一样的
    
int msgflg    	这个参数依然是控制函数行为的标志,取值可以是:0,表示忽略。

在 msgrcv 函数中,最后一个参数 flag 控制着接收消息的行为。这个参数的值可以是 0 或者一些其他的标志,它们影响着函数的阻塞行为以及消息的接收方式。

当 flag 参数为 0 时,表示接收消息的行为是阻塞的。也就是说,如果当前消息队列中没有符合条件的消息可供接收,msgrcv 函数会一直等待,直到有合适的消息到达为止。这样的阻塞模式通常用于需要等待消息到达的场景,使得进程能够在没有消息时挂起,直到有消息可用。

除了 0 之外,还有其他的一些标志可以传递给 flag 参数,例如:

IPC_NOWAIT: 表示非阻塞模式,如果没有符合条件的消息可供接收,函数会立即返回,并返回一个错误码(例如 -1),而不会等待消息到达。
MSG_NOERROR: 表示在接收消息时,如果消息长度超过了缓冲区的大小,则截断消息而不会产生错误。
其他一些标志,具体取决于系统的实现和特性。
因此,当 flag 参数为 0 时,表示 msgrcv 函数以阻塞模式接收消息,直到有消息到达为止。
    
/*	函数说明:从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除	*/   

3.5 控制消息队列函数msgctl()原型和头文件:

/*
	Linux下 man 2 msgctl查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

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

int						函数返回值,成功返回0,失败返回-1,错误原因存于error中
    
int msqid				由msgget函数返回的消息队列标识码
    
int cmd					是将要采取的动作(有三个可取值)一般用IPC_RMID,这时候表示移除消息队列的意思  
1. IPC_STAT:读取消息队列的数据结构msqid_ds,并将其存储在b u f指定的地址中。
2. IPC_SET:设置消息队列的数据结构msqid_ds中的ipc_perm元素的值。这个值取自buf参数。
3. IPC_RMID:从系统内核中移走消息队列。

struct msqid_ds *buf	消息队列管理结构体,请参见消息队列内核结构说明部分,一般buf = NULL;

/*	函数说明:获取和设置消息队列的属性	*/

3.6 获取消息队列函数ftok()原型和头文件:

/*
	Linux下 man ftok查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

key_t					函数返回值,成功返回key_t值(即IPC 键值),失败返回-1
    
const char *pathname	指定的文件名(已经存在的文件名),一般使用当前目录,比如:
                        key_t key;
                        key = ftok(".",1);	//这样就是将pathname设为当前目录

int proj_id				子序号。虽然是int类型,但是只使用8bits(1-255)
 
/*函数说明:
系统IPC键值的格式转换函数,系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函	数得到。
通过ftok函数生成key值,函数ftok把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键值(也称IPC key键值)。
*/

3.7 使用消息队列实现两个进程之间的通信:

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

struct msgbuf {

    long mtype;       /* message type, must be > 0 */
    char mtext[128];  /* message data */
};

int main()
{
    key_t key;
    struct msgbuf sendBuf = {888,"this is message from quen"};
    struct msgbuf readBuf;

    //key_t ftok(const char *pathname, int proj_id);
    key = ftok(".",66);                                  //获取IPC键值
    printf("key = 0x%x\n",key);

    //int msgget(key_t key, int msgflg);
    int msgId = msgget(key,IPC_CREAT|0777);              //打开这个消息队列,如果没有则创建它,权限为可读可写可执行
    if(msgId == -1){
        printf("创建消息队列失败\n");
    }

    //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);         //把sendBuf.mtext的内容发送到msgId对应的消息队列当中

    //ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),988,0);     //把msgId对应的消息队列中的内容读取到readBuf.mtext里面
    printf("从消息队列读取出来的内容是:%s\n",readBuf.mtext);
    
    msgctl(msgId,IPC_RMID,NULL);                            //从系统内核中移走消息队列

    return 0;
}
/*msgGet.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf {

    long mtype;       /* message type, must be > 0 */
    char mtext[128];  /* message data */
};

int main()
{
    key_t key;
    struct msgbuf readBuf;
    struct msgbuf sendBuf = {988,"谢谢你的到来!\n"};
 
    //key_t ftok(const char *pathname, int proj_id);
    key = ftok(".",66);                                  //获取IPC键值
    printf("key = 0x%x\n",key);

    //int msgget(key_t key, int msgflg);
    int msgId = msgget(key,IPC_CREAT|0777);              //打开这个消息队列,如果没有则创建它,权限为可读可写可执行
    if(msgId == -1){
        printf("创建消息队列失败\n");
    }

    //ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);     //把消息队列中的内容读取到readBuf里面
    printf("从消息队列读取出来的内容是:%s\n",readBuf.mtext);

    //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);         //把sendBuf.mtext的内容发送到msgId对应的消息队列当中

    msgctl(msgId,IPC_RMID,NULL);                            //从系统内核中移走消息队列
    
    return 0;
}

我们可以看到两个进程可以在同一个消息队列中进行通信。

四、共享内存

  • 共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。

4.1 共享内存的特点:

  1. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

  2. 因为多个进程可以同时操作,所以需要进行同步。

  3. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问(共享内存实现的进程间通信底层不提供任何同步与互斥机制。如果想让两进程很好的合作起来,在IPC里要有信号量来支撑。)

4.2 用指令来查看和释放已经存在的共享内存:

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

ipcrm -m shmid		//释放系统中的已有共享内存段(shmget返回值)

4.3 共享内存创建/获取函数shmget()原型和头文件:

/*
	Linux下 man 2 shmget查看手册
*/
#include <sys/ipc.h>
#include <sys/shm.h>

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

int				函数返回值,成功返回共享内存的标识符ID,失败则返回-1
    
key_t key		通常要求此值来源于ftok返回的IPC键值
1. 0(IPC_PRIVATE):会建立新共享内存对象
2. 大于0的32位整数:视参数shmilg来确定操作。    
    
size_t size		共享内存的大小		
1. 大于0的整数:新建的共享内存大小,以字节为单位
2. 0:只获取共享内存时指定为0
    
int shmflg		权限标志,常用两个IPC_CREAT和IPC_EXCL,一般后面还加一个权限,相当于文件的权限 
1. IPC_CREAT:创建一个共享内存返回,已存在打开返回
2. IPC_EXCL:配合着IPC_CREAT使用,共享内存已存在出错返回。
一般使用:IPC_CREAT | IPC_EXCL | 0666    
    
/*函数说明:得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符*/

4.4 共享内存映射函数shmat()原型和头文件:

/*
	Linux下 man 2 shmat查看手册
*/
#include <sys/types.h>
#include <sys/shm.h>

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

void *					函数返回值,成功返回指向共享内存的地址,失败返回-1,错误原因在erron中
    
int shmid				共享内存标识符,shmget的返回值
const void *shmaddr		指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
int shmflg				SHM_RDONLY:为只读模式,其他为读写模式,设为0系统默认   
    
/*函数说明:
shmat(把共享内存区对象映射到调用进程的地址空间),连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问
*/  

4.5 断开与共享内存的连接函数shmdt()原型和头文件:

/*
	Linux下 man 2 shmdt查看手册
*/
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

int						函数返回值,成功返回0,失败返回-1
const void *shmaddr		连接的共享内存的起始地址,shmat的返回值  
    
/*函数说明:
断开与共享内存的连接,与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存
*/   

4.6 共享内存控制/删除函数shmctl()原型和头文件:

/*
	Linux下 man 2 shmctl查看手册
*/
#include <sys/ipc.h>
#include <sys/shm.h>

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

int						函数返回值,成功返回0,失败返回-1
int shmid				共享内存标识符
    
int cmd					共享内存控制的方式
1. IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
2. IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
3. IPC_RMID:删除这片共享内存
    
struct shmid_ds *buf	 共享内存管理结构体,具体说明参见共享内存内核结构定义部分。删除共享内存的时候,一般设置为NULL
    
/*函数说明:完成对共享内存的控制*/    

4.7 使用共享内存实现两个进程之间的通信:

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

int main()
{
    key_t key;
    int shmId;
    char *shmaddr;
    
    //key_t ftok(const char *pathname, int proj_id);
    key = ftok(".",1);                                  //获取IPC键值  
    if(key == -1){
        printf("获取IPC键值失败\n");
    }
    
    //int shmget(key_t key, size_t size, int shmflg);
    shmId = shmget(key,1024*4,IPC_CREAT|0666);          //创建一个共享内存
    if(shmId == -1){
        printf("创建共享内存失败\n");
        exit(-1);
    }

    //void *shmat(int shmid, const void *shmaddr, int shmflg);
    shmaddr = shmat(shmId,NULL,0);                      //把共享内存区对象映射到调用进程的地址空间
    if(*shmaddr == -1){
        printf("映射共享内存失败\n");
    }
    strcpy(shmaddr,"chenlichen handsome");              //往共享内存里面发送数据
    sleep(5);                                           //避免写程序运行太快,避免读程序读不到

    //int shmdt(const void *shmaddr);
    int dt = shmdt(shmaddr);                            //断开与共享内存的连接
    if(dt == -1){
        printf("共享内存断开连接失败\n");
    }

    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    //shmctl(shmId,IPC_RMID,NULL);                        //删除共享内存
    printf("退出\n");
    return 0;
}
/*shmread.c*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
    key_t key;
    int shmId;
    char *shmaddr;
    
    //key_t ftok(const char *pathname, int proj_id);
    key = ftok(".",1);                                  //获取IPC键值  
    if(key == -1){
        printf("获取IPC键值失败\n");
    }
    
    //int shmget(key_t key, size_t size, int shmflg);
    shmId = shmget(key,1024*4,IPC_CREAT|0666);          //创建一个共享内存
    if(shmId == -1){
        printf("创建共享内存失败\n");
        exit(-1);
    }

    //void *shmat(int shmid, const void *shmaddr, int shmflg);
    shmaddr = shmat(shmId,NULL,0);                      //把共享内存区对象映射到调用进程的地址空间
    if(*shmaddr == -1){
        printf("映射共享内存失败\n");
    }
    printf("从共享内存中读取到的内容是:%s\n",shmaddr);

    //int shmdt(const void *shmaddr);
    int dt = shmdt(shmaddr);                            //断开与共享内存的连接
    if(dt == -1){
        printf("共享内存断开连接失败\n");
    }

    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    shmctl(shmId,IPC_RMID,NULL);                        //删除共享内存
    printf("退出\n");
    return 0;
}

五、信号

  • 对于Linux来说,实际信号是软中断,许多重要的程序都需要处理信号,信号为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。

5.1 信号的名称和编号:

  • 每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。

  • 信号定义在signal.h头文件中,信号名都定义为正整数

  • 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号,kill对于信号0又特殊的应用

信号描述
SIGHUP挂起。用来在终端连接丢失的时候通报
SIGINT中断。当用户点击中断(Ctrl-c)键时发送
SIGABRT通过C函数abort()发送;为assert()使用
SIGKILL迅速完全终止进程;不能被捕获
SIGUSR1用户定义的信号1
SIGSEGV段错误;在一些操作系统中;它被理解为综合的保护措施
SIGUSR2用户定义的信号2
SIGPIPE当进程读取的一个管道突然关闭时发送
SIGALRM当一个警报超时的时候发送
SIGTERM常规地终止进程
SIGCHLD一个子进程刚刚终止或者改变状态
SIGCONT用SIGSTOP停止后继续运行
SIGSTOP中断进程;不能被捕获
SIGTSTP中断进程;可以被捕获
SIGTTIN终端输入造成的进程停止
SIGTTOU终端输出造成的进程停止
SIGCXPUCPU超时
SIGXFSZ文件大小超过限度
SIGBABY当一个子进程需要PHP父程序来处理的时候传递

5.2 信号的处理:

  • 信号的处理有三种方法,分别是:忽略捕捉默认动作

  1. 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景

  2. 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。

  3. 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。

  • 具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。

5.3 信号的应用之杀死进程:

其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。比如,在后台运行了一个进程,通过 ps 命令可以查看这个进程的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了该进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程

#include <stdio.h>

int main()
{
	while(1);
	return 0;
}

由于这个程序没有停止条件,a.out程序会一直运行,除了用ctrl+c和ctrl+z两个命令来结束该进程外,还有用kill -9 PID来结束,先用指令-aux|grep a.out查看a.out进程的PID,a.out的PID是41955,输入kill -9 41955或者kill SIGKILL 41955都可以杀死进程,对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段。

5.4 信号处理函数的注册和信号处理发送函数:

  • 信号处理函数的注册不只一种方法,分为入门版高级版

  1. 入门版:signal

  2. 高级版:sigaction

  • 信号发送函数也不止一个,同样分为入门版和高级版

  1. 入门版:kill

  2. 高级版:sigqueue

5.5 信号注册函数——入门版

5.5.1 信号注册函数signal()原型和头文件:
/*
	Linux下 man 2 signal查看手册
*/
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

sighandler_t			函数返回值,返回信号处理程序的前一个值,或者在错误时SIG ERR。如果发生错误,则设置errno来指示原因。

int signum				指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号

sighandler_t handler	描述了与信号关联的动作,它可以取以下三种值:  
    
1. 一个无返回值的函数地址
此函数必须在signal()被调用前声明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。这个函数应有如下形式的定义:
void handler(int signum);

2. SIG_IGN
这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。
    
3. SIG_DFL
这个符号表示恢复系统对信号的默认处理。

 根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。 对于sighandler_t signal(int signum, sighandler_t handler);函数来说,signum 显然是信号的编号handler 是中断函数的指针。 同样,typedef void (*sighandler_t)(int);中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。

5.5.2 捕捉信号:
#include <stdio.h>
#include <signal.h>

void handler(int signum)
{
    printf("get signum = %d\n",signum);
    switch(signum){
        case 2:
            printf("SIGINT\n");
            break;
        case 9:
            printf("SIGKILL\n");
            break;
        case 10:
            printf("SIGUSR1\n");
            break;
    }

    printf("never quit\n");
}

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

    signal(SIGINT,handler);     //捕捉Ctrl + C信号
    signal(SIGKILL,handler);    //捕捉杀死进程信号
    signal(SIGUSR1,handler);    //捕捉用户自定义的信号1
    while(1);
    return 0;
}

5.5.4 忽略信号(SIG_IGN):
#include <stdio.h>
#include <signal.h>

void handler(int signum)
{
    printf("get signum = %d\n",signum);
    switch(signum){
        case 2:
            printf("SIGINT\n");
            break;
        case 9:
            printf("SIGKILL\n");
            break;
        case 10:
            printf("SIGUSR1\n");
            break;
    }

    printf("never quit\n");
}

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

    signal(SIGINT,SIG_IGN);     //忽略Ctrl + C信号
    signal(SIGKILL,SIG_IGN);    //忽略杀死进程信号
    signal(SIGUSR1,SIG_IGN);    //忽略用户自定义的信号1
    while(1);
    return 0;
}

  • 对 Ctrl+C/SIGINT 和 用户自定义信号1/SIGUSR1起忽略作用,但对 SIGKILL 不可忽略。

5.6 信号发送函数——入门版

5.6.1 信号发送函数kill()原型和头文件:
/*
	Linux下 man 2 kill查看手册
*/
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

int 			函数返回值,如果成功(至少发了一个信号)则返回0,失败则返回-1

pid_t pid		指定接收进程的进程ID
int sig			信号编号或者是信号的类型,可以通过kill -l指令查看
5.6.2 使用信号发送函数kill()实现信号的发送:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>

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

    signum = atoi(argv[1]);         //把字符串转换成整数
    pid = atoi(argv[2]);

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

    //int kill(pid_t pid, int sig);
    int ret = kill(pid,signum);     //把signum的信号发送到对应的ID进程中
    if(ret == -1){
        printf("发送信号失败\n");   
    }

    return 0;
}

5.6.3 使用sprintf和system完成与kill一样的功能:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    int pid;
    int signum;
    char str[128] = {0};

    pid = atoi(argv[1]);        //把字符串转换成整数
    signum = atoi(argv[2]);

    sprintf(str,"kill %d %d",signum,pid);
    printf("signum = %d,pid = %d\n",signum,pid);

    system(str);
    return 0;
}

5.7 信号注册函数——高级版

  • 为什么会有高级版,我们的入门版虽然可以发出和接收到了信号,但我们想发出信号的同时携带点数据,这时候需要用到高级版 sigaction。

5.7.1 信号接收函数sigaction()原型和头文件:
/*
	Linux下 man 2 sigaction查看手册
*/
#include <signal.h>

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

struct sigaction {
   void     (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void     (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int      sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};
//回调函数句柄sa_handler、sa_sigaction只能任选其一
  • sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中:

第一个参数signum:是注册的信号的编号,可以使用kill -1指令查看

第二个参数act:如果不为空说明需要对该信号有新的配置;

第三个参数oldact:如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

======================================================================================================

  • 结构体struct sigaction说明:

(1)void (*sa_handler)(int); 不携带数据,作用与入门版类似。

(2)void (*sa_sigaction)(int, siginfo_t *, void *); 处理函数来说还需要有一些说明。

  • int 是接收注册信息的编号;

  • void * 是接收到信号所携带的额外数据;

  • struct siginfo 这个结构体主要适用于记录接收信号的一些相关信息;

注意sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。

(3)sa_mask sigset_t sa_mask是一个信号集,在调用该信号捕捉函数之前,将需要block的信号加入这个sa_mask,仅当信号捕捉函数正在执行时,才阻塞sa_mask中的信号,当从信号捕捉函数返回时进程的信号屏蔽字复位为原先值;因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。

(4)sa_flags 是一个选项,注意:这个选项只与sigaction函数注册的信号有关联,与sa_mask中的信号无任何关系

  • SA_INTERRUPT 由此信号中断的系统调用不会自动重启

  • SA_RESTART 由此信号中断的系统调用会自动重启

  • SA_SIGINFO 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针

  • SA_NODEFER 一般情况下,当信号处理函数运行时,内核将阻塞(sigaction函数注册时的信号)。但是如果设置了SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。

  • SA_RESETHAND 当调用信号处理函数时或信号处理函数结束后,将信号的处理设置为系统默认值。

======================================================================================================

  • struct siginfo结构体说明

void (*sa_sigaction)(int signum,siginfo_t *info,void *ucontext);

其中void *是接收到信号所携带的额外数据;而struct siginfo这个结构体主要适用于记录接收信号的一些相关信息

siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}

关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时在si_int或者si_ptr成员中也保存了对应的数据。

5.8 信号发送函数——高级版

5.8.1 信号发送函数sigqueue()原型和头文件:
/*
	Linux下 man 3 sigqueue查看手册
*/
#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);

int 				函数返回值,如果sigqueue()返回0,表示信号已成功排队到接收进程。否则,返回-1,并设置errno来表示错误。

pid_t pid			目标进程的进程ID
int sig				信号编号或者是信号的类型,可以通过kill -l指令查看    
    
union sigval value	一个联合体,表示信号附带的数据,附带数据可以是一个整数也可以是一个指针,有如下形式:
    
union sigval {
        int   sival_int;
        void *sival_ptr;
};

5.9 使用信号接收函数sigaction()和信号发送函数sigqueue()实现两个进程的通信:

/*NiceSignal.c*/
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int signum, siginfo_t *info, void *context)    //信号处理函数
{
    printf("signum = %d\n",signum);

    if(context != NULL){
        printf("获取到的PID = %d\n",info->si_pid);
        printf("获取到的数据是:%d\n",info->si_int);
        printf("获取到的数据是:%d\n",info->si_value.sival_int);
    }
}

int main()
{
    struct sigaction act;

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

    act.sa_sigaction = handler;
    act.sa_flags = SA_SIGINFO;      //将标志位指定为此才可接收数据
    //int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
    sigaction(SIGINT,&act,NULL);

    while(1);
    return 0;
}
/*send.c*/
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    if(argc != 3){
        printf("请重新输出3个参数\n");
        exit(-1);
    }
    int pid;
    int signum;

    union sigval value;

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

    value.sival_int = 100;
    //int sigqueue(pid_t pid, int sig, const union sigval value);
    int ret = sigqueue(pid,signum,value);
    if(ret == -1){
        printf("sigqueue发送信号失败\n");
    }
    printf("PID = %d\n",getpid());
    printf("done\n");
    return 0;
}

使用信号发送函数sigqueue()将SIGINT信号发送到93946所对应的进程中了,发送的内容是100,信号接收端能够接收到来自73980进程发送的信号和内容,并且能够得到发送进程的进程ID,从而实现了两个进程间用信号的通信

六、信号量

  • 信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据Linux下的信号量函数都是在通用的信号量数组上进行操作,而不是 一个单一的二值信号量上进程操作,二值信号量:信号量只能取0或者1的变量

6.1 信号量相关定义:

  • 临界资源:能被多个进程共享,但一次只能允许一个进程使用的资源称为临界资源。

  • 临界区:涉及到临界资源的部分代码,称为临界区。

  • 互斥:亦称间接制约关系,在一个进程的访问周期内,另一个进程就不能进行访问,必须进行等待。当占用临界资源的进程退出临界区后,另一个进程才允许去访问此临界资源。

    例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时, 系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。

  • 同步:亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的同步就是源于它们之间的相互合作。所谓同步其实就是两个进程间的制约关系。

    例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。

  • 原子性:对于进程的访问,只有两种状态,要么访问完了,要么不访问。当一个进程在访问某种资源的时候,即便该进程切出去,另一个进程也不能进行访问。

6.2 信号量特点:

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存

  2. 信号量基于操作系统的 PV 操作,P(拿锁)V (放回锁)程序对信号量的操作都是原子操作。

  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  4. 支持信号量组

6.3 创建/获取一个信号量组函数semget()原型和头文件:

/*
	Linux下 man 2 semget查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

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

int 		函数返回值,成功返回信号量标识符,再信号量的其他函数中都会使用该值,出错则返回-1
    
key_t key	信号量的键值,多个进程可以通过它访问同一个信号量(通过ftok获取)
int nsems	信号量集中信号量的个数
    
int semflg	标识函数的行为及权限。取值如下:    
1. IPC_CREAT:如果不存在就创建
2. IPC_EXCL和IPC_CREAT搭配使用,如果已经存在,则返回失败

/*运用实例*/
semid = semget(key,1,IPC_CREAT|0666);	//获取或创建信号量

6.4 控制信号量函数semctl()原型和头文件:

/*
	Linux下 man 2 semctl查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

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

int 		函数返回值,成功根据cmd的值不同返回不同的值。SEM_STAT
    
int semid			semget()函数返回的信号量标识符,要操作的信号量
int semnum			信号量编号,当使用信号量集才会被用到。通常取值为0,就是使用单个信号量(也就是第一个信号量)
    
int cmd				将要采取的动作,取值如下:
1. SETVAL:设置信号量集中信号量的计数值
2. GETVAL:获取信号量集中信号量的计数值
3. IPC_STAT:把semid_ds结构中的数据设置为信号量集的当前关联值
4. IPC_SET:在进程有足够权限的前提下,把信号集的当前关联值设置为semid_ds数据结构中给出的值
5. IPC_RMID:删除信号量集,不要第四个参数
    
union semun arg		arg是一个union semun结构,当第三个参数为SETVAL时即初始化信号量的值(信号量成功创建后,需要设置初始						值),这个值由第四个参数决定。第四参数是一个自定义的共同体,如下:    
union semun {
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                   (Linux-specific) */
};

/*运用实例*/
union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
};

union semun set;
set.val = 1;  					//信号量集中的第几个信号
semctl(semid,0,SETVAL,set);		//初始化信号量
/*SETVAL设置信号量的值,这里设置为set*/

6.5 操作信号量组函数semop()原型和头文件:

/*
	Linux下 man 2 semop查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

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

int 					函数返回值,成功返回0,失败返回-1
    
int semid				semget()函数返回的信号量标识符
    
size_t nsops			操作数组sops中的操作个数/信号量的个数,通常取值为1(一个操作)
    
struct sembuf *sops		指向信号量操作数组,一个数组包括以下成员:

struct sembuf{   
    
     unsigned short sem_num;
     short sem_op;                
     short sem_flg;          
};

unsigned short sem_num;		信号量的编号,使用单个信号量时通常取值为0
    
short sem_op;				信号量一次PV操作时加减的数值,一般只会用到两个值:
-1:P操作,等待信号变得可用
+1:V操作,发出信号量已经变得可用
    
short sem_flg;				标志,有两个取值:(通常设置为SEM_UNDO)
1. SEM_NOWAIT:表示队列满不等待,非阻塞,返回EAGAIN错误。
2. SEM_UNDO: 使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量 
    
/*运用实例*/
struct sembuf init;
init.sem_num = 0;
init.sem_op = -1;
init.sem_flg = 0;

semop(semid,&init,1);

6.6 使用信号量来控制父子进程的执行顺序问题:

  • 使用信号量编程来控制父进程和子进程,保证父进程在子进程执行完毕后执行:

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

union semun {  
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO*/
};

/*p操作*/
void pGetKey(int semid)
{
    struct sembuf set;

    set.sem_num = 0;        //信号量编号
    set.sem_op = -1;        //P操作,等待信号变得可用(拿钥匙)
    set.sem_flg = SEM_UNDO; //使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
    // int semop(int semid, struct sembuf *sops, size_t nsops);
    semop(semid, &set, 1);

    printf("pGetKey OK\n");
}

/*v操作*/
void vPutBackKey(int semid)
{
    struct sembuf set;

    set.sem_num = 0;        //信号量编号
    set.sem_op = 1;         //V操作,发出信号量已经变得可用
    set.sem_flg = SEM_UNDO; //使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量

    // int semop(int semid, struct sembuf *sops, size_t nsops);
    semop(semid, &set, 1);

    printf("vPutBackKey OK\n");
}

int main()
{
    key_t key;
    int semid;
    union semun initsem;
    int retpid;

    //key_t ftok(const char *pathname, int proj_id);
    key = ftok(".",2);                          //获取IPC键值
    if(key == -1){
        printf("创建IPC键值失败\n");
    }
    //int semget(key_t key, int nsems, int semflg);
    semid = semget(key, 1, IPC_CREAT|0666);     //创建/获取一个信号量
    if(semid == -1){
        printf("创建/打开信号量失败\n");
    }

    initsem.val = 0;
    //int semctl(int semid, int semnum, int cmd, ...);
    semctl(semid, 0, SETVAL, initsem);          //设置信号量集中的信号量计数值

    retpid = fork();                            //创建一个子进程,用pv操作,使得子进程执行完毕后在执行父进程
    if(retpid > 0){                             //pid > 0代表进入父进程
        pGetKey(semid);                         //先去拿锁,若子进程已经执行完,则存在锁,可执行
        printf("进入父进程\n");
        vPutBackKey(semid);                     //父进程执行完成后放回锁(钥匙)
    }else if(retpid == 0){                      //pid == 0代表进入子进程
        printf("进入子进程\n");
        vPutBackKey(semid);                     //子进程执行完毕后创建锁(钥匙)
        semctl(semid,0,IPC_RMID);               //删除信号量集
    }else{
        printf("创建子进程失败\n");
        exit(-1);
    }

    return 0;
}

6.7 使用信号量来控制共享内存读写顺序问题:

/*semshmwrite.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <string.h>

union semun {    
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO*/
};

void pGetKey(int semid)
{
    struct sembuf set;

    set.sem_num = 0;        //信号量编号
    set.sem_op = -1;        //P操作,等待信号变得可用(拿钥匙)
    set.sem_flg = SEM_UNDO; //使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
    // int semop(int semid, struct sembuf *sops, size_t nsops);
    semop(semid, &set, 1);

    printf("pGetKey OK\n");
}

/*v操作*/
void vPutBackKey(int semid)
{
    struct sembuf set;

    set.sem_num = 0;        //信号量编号
    set.sem_op = 1;         //V操作,发出信号量已经变得可用
    set.sem_flg = SEM_UNDO; //使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量

    // int semop(int semid, struct sembuf *sops, size_t nsops);
    semop(semid, &set, 1);

    printf("vPutBackKey OK\n");
}

int main()
{
    key_t shmkey;
    key_t semkey;
    int shmid;
    int semid;
    char *addr = NULL;
    char *str = "chenlichen handsome";

    //key_t ftok(const char *pathname, int proj_id);
    shmkey = ftok(".",2);                                   //创建共享内存IPC键值
    if(shmkey == -1){
        printf("创建共享内存IPC键值失败\n");
    }

    semkey = ftok(".",4);                                   //创建信号量IPC键值
    if(semkey == -1){
        printf("创建信号量IPC键值失败\n");
    }

    printf("共享内存IPC键值key = 0x%x\n",shmkey);
    printf("信号量IPC键值key = 0x%x\n",semkey);

    //int shmget(key_t key, size_t size, int shmflg);
    shmid = shmget(shmkey, 0, 0);                           //创建共享内存,无需开辟空间,服务器读进程已经开辟空间
    if(shmid == -1){
        printf("创建共享内存失败\n");
        exit(-1);
    }

    //int semget(key_t key, int nsems, int semflg);
    semid = semget(semkey, 1, IPC_CREAT|0666);              //创建/获取一个信号量
    if(semid == -1){
        printf("创建信号量失败\n");
        exit(-1);
    }

    union semun set;
    set.val = 0;
    //int semctl(int semid, int semnum, int cmd, ...);
    semctl(semid, 0, SETVAL, set);                          //设置信号量集中的信号量计数值

    //void *shmat(int shmid, const void *shmaddr, int shmflg);
    addr = shmat(shmid, NULL, 0);                           //把共享内存区对象映射到调用进程的地址空间
    if(*addr == -1){
        printf("映射共享内存失败\n");
    }
    strcpy(addr,str);                                       //把str中的内容写入到共享内存中
    vPutBackKey(semid);                                     //客户端向共享内存写完内容后,创建锁

    //int shmdt(const void *shmaddr);
    int dt = shmdt(addr);                                   //断开与共享内存的连接
    if(dt == -1){
        printf("断开与共享内存连接失败\n");
    }
    return 0;
}
/*semshmread.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <stdlib.h>

union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO*/
};

void pGetKey(int semid)
{
    struct sembuf set;

    set.sem_num = 0;        //信号量编号
    set.sem_op = -1;        //P操作,等待信号变得可用(拿钥匙)
    set.sem_flg = SEM_UNDO; //使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
    // int semop(int semid, struct sembuf *sops, size_t nsops);
    semop(semid, &set, 1);

    printf("pGetKey OK\n");
}

/*v操作*/
void vPutBackKey(int semid)
{
    struct sembuf set;

    set.sem_num = 0;        //信号量编号
    set.sem_op = 1;         //V操作,发出信号量已经变得可用
    set.sem_flg = SEM_UNDO; //使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量

    // int semop(int semid, struct sembuf *sops, size_t nsops);
    semop(semid, &set, 1);

    printf("vPutBackKey OK\n");
}

int main()
{
    key_t shmkey;
    key_t semkey;
    int shmid;
    int semid;
    char *addr = NULL;

    //key_t ftok(const char *pathname, int proj_id);
    shmkey = ftok(".",2);                                   //创建共享内存IPC键值
    if(shmkey == -1){
        printf("创建共享内存IPC键值失败\n");
    }

    semkey = ftok(".",4);                                   //创建信号量IPC键值
    if(semkey == -1){
        printf("创建信号量IPC键值失败\n");
    }

    printf("共享内存IPC键值key = 0x%x\n",shmkey);
    printf("信号量IPC键值key = 0x%x\n",semkey);

    // int shmget(key_t key, size_t size, int shmflg);
    shmid = shmget(shmkey, 1024*4, IPC_CREAT|IPC_EXCL|0666);   //创建一个共享内存
    if(shmid == -1){
        printf("创建共享内存失败\n");
        exit(-1);
    }

    //int semget(key_t key, int nsems, int semflg);
    semid = semget(semkey, 1, IPC_CREAT|0666);                  //创建一个信号量
    if(semid == -1){
        printf("创建信号量失败\n");
        exit(-1);
    }

    union semun set;
    set.val = 0;
    //int semctl(int semid, int semnum, int cmd, ...);
    semctl(semid, 0, SETVAL, set);                              //设置信号量集中的信号量计数值

    pGetKey(semid);                                             //当客户端向共享内存完写数据时,在读写
    // void *shmat(int shmid, const void *shmaddr, int shmflg);
    addr = shmat(shmid, NULL, 0);                               //把共享内存区对象映射到调用进程的地址空间
    if(*addr == -1){
        printf("映射共享内存失败\n");
    }
    vPutBackKey(semid);                                         //解锁
    printf("从共享内存中读取出的数据是:%s\n",addr);

    //int semctl(int semid, int semnum, int cmd, ...);
    semctl(semid, 0, IPC_RMID);                                 //删除信号量集

    // int shmdt(const void *shmaddr);
    int dt = shmdt(addr);                                       //断开与共享内存之间的连接
    if(dt == -1){
        printf("断开与共享内存之间的连接失败\n");
    }

    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    int sh = shmctl(shmid, IPC_RMID, NULL);                     //释放共享内存
    if(sh == -1){
        printf("释放共享内存失败\n");
    }

    return 0;
}

总结

  1. 管道:速度慢,容量有限,只有父子进程能通讯。

  2. FIFO:任何进程间都能通讯,但速度慢。

  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。

  4. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

  5. 信号量:不能传递复杂消息,只能用来同步。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值