Linux环境下的进程间的通信

版权声明:本文为博主原创文章,如果喜欢欢迎收藏转载!如有错误,请指出! https://blog.csdn.net/h___q/article/details/83686537

进程间通信的方式

管道

  • 管道是Linux操作系统下的一种文件类型,该文件类型的文件就是为了给不同进程之间提供可以进行通信的文件资源。
  • 一个管道只能单向通信。

为什么一个管道只能进行单向通信?

一个进程可以对一个管道文件进行读或写操作,当两个进程通过一个管道进行通信时,一定是有一个进程对管道进行写操作,另一个管道进行读操作,这样就可以实现进程之间的单向通信,但假如两个进程同时将读写端口都打开时,两个进程都可以进行读或写操作,这样就会造成进程之间读取信息紊乱,所以一个管道只能进行单向通信,如果想要以管道的方式实现两个进程之间双向通信,就必须实现两个管道。

管道操作

图解管道操作(以匿名管道为例):

1.父进程创建管道

iR5OoV.png

2.父进程fork出子进程

iRIpQJ.md.png

3.子进程关闭写端,父进程关闭读端

iRIVJO.png

所以我们可以将管道视为文件,并且使用文件操作的方式去操作管道

匿名管道

  • 匿名管道只能用于有亲缘关系的两个进程之间的通信,通常用于子进程与父进程
  • 匿名管道属于管道的一种,它同样只可以单向通信
  • 匿名管道的生命周期随进程
  • 匿名管道是面向字节流的
  • 操作系统内核会对匿名管道进行同步于互斥

接口

#include<unistd.h>

int pipe(int fd[2]);

接口功能:创建一个匿名管道文件
参数:文件描述符数组,fd[0]表示读端,fd[1]表示写端

代码实现匿名管道

/*
 *子进程向管道内写入10次,i am child...
 *父进程每隔1s从管道本读出一句i am child...
 *该进程的子进程与父进程未设置退出逻辑,需要使用[Ctrl+C]终止进程
 */
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
void test_pipe()
{
    int fd[2];
    if(-1 == pipe(fd))//匿名管道创建失败
    {
        exit(EXIT_FAILURE);
    }
    pid_t id = fork();
    char buf[64];
	if(-1 == id)//子进程创建失败
    {
        exit(EXIT_FAILURE);
    }
    else if(0 == id)//子进程
    {
        int i = 0;
        close(fd[0]);//关闭子进程读端
        strcpy(buf, "i am child...\n");
        while(i < 10)
        {
            write(fd[1], buf, sizeof(buf));
            i += 1;
        }
        
        while(1);//子进程写入完成时不退出,管道写端不关闭
        close(fd[1]);
    }
    else//父进程
    {
        close(fd[1]);//关闭父进程的写端
        while(1)
        {
            read(fd[0], buf, sizeof(buf));//子进程从管道读数据
            printf("%s\n", buf);
        }
        
        while(wait(NULL)!=id);//等待子进程退出
        close(fd[0]);//关闭读端
    }
}
int main()
{
    test_pipe();
    return 0;
}
运行结果:
i am child...
i am child...
i am child...
i am child...    
i am child...
i am child...
i am child...
i am child...
i am child...
i am child...

命名管道

  • 命名管道可以应用于两个毫不相干的进程之间的通信
  • 命名管道是一种特殊类型的文件

接口

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

int mkfifo(const char* pathname, mode_t mode);
功能:在pathname路径下,创建一个命名管道文件

参数:
	pathname:路径信息,表示管道文件将会创建在pathname路径下,不支持~
	mode:管道文件权限信息,通常配合umask(mode_t mode);使用

命名管道与匿名管道的区别

  • 匿名管道只能用于具有亲缘关系的进程之间的通信,命名管道可以用于任意两个进程之间的通信。
  • 匿名管道用pipe()函数创建并打开,命名管道通过mkfifo()函数创建,由open()函数打开

代码实现命名管道

通过命名管道实现client进程向service进程发送文本数据,该管道由service创建并销毁,client只打开并写入文本数据即可。client.c service.c pipe.h必须放在同一级目录下。

模代码结构

$ ls
service.c client.c pipe.h Makefile
$ cat Makefile
.PHONY:all
all:service client
service:service.c
	gcc $^ -o $@
client:client.c
	gcc $^ -o $@
	
.PHONY:clean
clean:
	rm -f service client
/*
 *pipe.h
 */
#ifndef _PIPE_H_
#define _PIPE_H_
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>

#define PATHNAME "/tmp/pipe_withname"
#define BUF_SIZE 1024
#denif
/*
 *service.c
 */
#include"./pipe.h"
int main()
{
    int fd;
    char buf[BUF_SIZE] = {0};
    umask(0);
    
    if(-1 == mkfifo(PATHNAME, 0644))//创建管道文件,文件权限为-rw-r--r--
    {
        exit(EXIT_FAILURE);//若管道创建失败,则失败退出
    }
    if(-1 == (fd = open(PATHNAME, O_RDONLY)))//只读打开管道文件
    {
        exit(EXIT_FAILURE);//如果打开失败,则失败退出
    }
    
    while(1)
    {
        ssize_t s;
        printf("please wait...\n");
        s = read(fd, buf, sizeof(BUF_SIZE));
        if(s <= 0)//s小于0,则读取出现异常
            break;
        if(0 == strcmp(buf, "quit\n"))//若client发送quit文本,则正常退出
        {
            printf("quit signal, Bye...\n");
            break;
        }
        printf("client said: %s", buf);
        fflush(stdout);
    }
    
    close(fd);
    return 0;
}
/*
 *client.c
 */
#include"./pipe.h"
int main()
{
    int fd;
    char buf[BUF_SIZE] = {0};
    if(-1 == (fd = open(PATHNAME, O_WRONLY)))//只写打开管道文件
    {
        exit(EXIT_FAILURE);//如打开失败,则失败退出
    }
    while(1)
    {
        printf("client: ");
        fflush(stdout);
        fgets(buf, sizeof(buf), stdin);//从键盘获取字符串
        write(fd, buf, sizeof(buf));//将获取到的字符串写进管道
        if(strcmp(buf, "quit\n"))
            break;
    }
    close(fd);
}

管道读写规则

  • 当没有数据可读时:

    • O_NONBLOCK disable:read调用阻塞,进程暂停执行,一直等到有数据来为止。
    • O_NONBLOCK enable:read返回-1,errno值为EAGAIN。
  • 当管道写满时:

    • O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
    • O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
  • 如果所有管道对应的写端文件描述符被关闭,则read返回0。

  • 如果所有管道对应的读端文件描述符被关闭,则write操作会产生信号SIGPIPE,可能会导致write退出。

  • 当要写入的数据不大于PIPE_BUF时,Linux保证写入数据的原子性。

  • 当要写入的数据大于PIPE_BUF时,Linux不再保证写入数据的原子性。


消息队列

什么是消息队列

  • 消息队列是操作系统内核提供的一个用于进程双方通信的队列(链表)
  • 进程向消息队列中发送的数据块中包含一个特有的类型,类似于标签,用来表明该数据块来自哪个进程。(因为进程向消息队列中发送的数据块不能被自己拿走)
  • 消息队列每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数是有上限的(MSGMNB),系统中消息队列的总数也是有上限的(MSGMNI)
  • 消息队列的生命周期是随系统的

IPC对象数据结构 /usr/include/linux/ipc.h

struct ipc_perm{
    key_t          _key; /* Key supplied to xxxget(2) */
    uid_t           uid; /* Effective UID of owner */
    gid_t           gid; /* Effective GID of owner */
    uid_t 		   cuid; /* Effective UID of Creater */
    gid_t          cgid; /* Effective GID of Creater */
    unsigned short mode; /* Permissions */
    unsigned short _seq; /* Sequeue number */
};
  • _key:用来判断两个进程看到的消息队列是否为同一个消息队列,每一个消息队列的_key值是唯一的
  • 进行通信的两个进程获取消息队列_key值的方式必须是相同的
  • ipc资源的生命周期跟随内核的
    • ipcs -q :查看系统中现存的消息队列
    • ipcrm -q msgid :删除消息队列

消息队列数据结构 /usr/include/linux/msg.h

struct msqid_ds {
 	struct ipc_perm msg_perm;
 	struct msg *msg_first; /* first message on queue,unused */
 	struct msg *msg_last; /* last message in queue,unused */
 	__kernel_time_t msg_stime; /* last msgsnd time */
 	__kernel_time_t msg_rtime; /* last msgrcv time */
 	__kernel_time_t msg_ctime; /* last change time */
 	unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
 	unsigned long msg_lqbytes; /* ditto */
 	unsigned short msg_cbytes; /* current number of bytes on queue */
 	unsigned short msg_qnum; /* number of messages in queue */
 	unsigned short msg_qbytes; /* max number of bytes on queue */
 	__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
 	__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};

消息队列中消息块的数据结构(自己定义)

struct msgbuf{
	long mytype;
    char mtext[1];
};
  • mtype:指明消息类型,它必须大于0,类似于标签,用来表明该数据块来自哪个进程
  • mtext[1]:存放消息数据,该数组的大小可以自行定义,但不能超过系统规定的上限值

接口:

//创建/打开消息队列
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

int msgget(key_t key, int msgflg);
  • key:用来判断两个进程看到的消息队列是否为同一个消息队列,每一个消息队列的_key值是唯一的。通常该处调用key_t ftok()函数来设置唯一键值
  • msgflg:
    • IPC_CREAT|IPC_EXEL:创建消息队列,如果想要创建的消息队列在底层已经存在,则出错返回;如果要创建的消息队列在底层不存在,则创建该消息队列,但不打开
    • IPC_CREAT:打开消息队列,如果想要打开的消息队列在底层已经存在,则打开该消息队列
  • 返回值:成功返回消息队列的标识符;失败返回-1
//删除消息队列
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • msqid:消息队列标识符
  • cmd:表示要对消息队列所做的操作
    • IPC_RMID:立即删除消息队列
    • IPC_STAT:
  • buf:属性信息,若要删除消息队列,此处可以传递NULL
//向消息队列发送数据
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
  • msqid:消息队列标识符
  • msgp:是一个指针,指向准备发送的消息
  • msgsz:是msgp指向的消息⻓度,这个⻓度不含保存消息类型的那个long int⻓整型
  • msgflg:控制当队列存满或达到系统上限时要做的事情
    • IPC_NOWAIT表示队列满不等待,返回EAGAIN错误
  • 返回值:成功返回0;失败返回-1
//从消息队列取出数据
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

ssize_t msgrcv(int msgid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
  • msgid:消息队列标识符
  • msgp:是⼀个指针,指针指向准备接收的消息
  • msgsz:是msgp指向的消息⻓度,这个⻓度不含保存消息类型的那个long int⻓整型
  • msgtype:它可以实现接收优先级的简单形式
    • msgtype=0返回队列第⼀条信息
    • msgtype>0返回队列第⼀条类型等于msgtype的消息
    • msgtype<0返回队列第⼀条类型⼩于等于msgtype绝对值的消息,并且是满⾜条件的消息类型最⼩的消息
  • msgflg:控制着队列中没有相应类型的消息可供接收时将要发⽣的事
    • msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误
    • msgflg=MSG_NOERROR,消息⼤⼩超过msgsz时被截断
    • msgtype>0且msgflg=MSG_EXCEPT,接收类型不等于msgtype的第⼀条消息
  • 返回值:成功返回实际放到接收缓冲区⾥去的字符个数,失败返回-1

封装消息队列接口

/*
 * 注意:1.接受消息时,如果消息队列里没有符合要求的消息,则会发生堵塞,直至消息队列中出现符和要求的消息后并读取它
 */
#ifndef _MESSAGE_H_
#define _MESSAGE_H_

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

#define PATHNAME "."
#define PRO_ID 0x6666

/*
 * type定义的值必须大于0
 */
#define SERVICE_TYPE 1
#define CLIENT_TYPE 2

struct msg_buf
{
  long type;
  char date[1024];
};

int CreatMsg();//创建消息队列,成功返回消息队列msgid,失败返回-1
int DestoryMsg(const int msgid);//删除消息队列,成功返回0,失败返回-1
int OpenMsg();//打开消息队列,打开成功返回消息队列msgid,失败返回-1
int SendMsg(int msgid,long who ,char* msg);//向消息队列发消息
int RecMsg(int msgid, long who,char msg[]);//从消息队列接受消息,成功返回0,并将消息写入msg,失败返回-1
#endif

//message.c
#include"./message.h"//使用时需确保头文件与message.c文件在同一级目录下

int CreatMsg()//创建消息队列,成功返回消息队列msgid,失败返回-1
{
  umask(0);
  return msgget(ftok(PATHNAME, PRO_ID),IPC_CREAT | IPC_SET | 0666);
}

int OpenMsg()//打开消息队列
{
  return msgget(ftok(PATHNAME, PRO_ID),IPC_CREAT);
}

int SendMsg(int msgid,long who ,char* msg)
{
  struct msg_buf buf;
  buf.type = who;
  strcpy(buf.date, msg);
  if(0 > msgsnd(msgid, (void*)&buf,sizeof(buf.date),0))
  {
    perror("msgsnd");
    return -1;
  }
  return 0;
}

int RecMsg(int msgid, long who, char  msg[])//从消息队列接受消息
{
  struct msg_buf buf;
  buf.type = who;
  printf("Please wait...\n");
  if(-1 == msgrcv(msgid, (void*)&buf, sizeof(buf.date), buf.type,0))
  {
    return -1;
  }
  strcpy(msg, buf.date);
  return 0;
}

int DestoryMsg(const int msgid)//删除消息队列,成功返回0,失败返回-1
{
  return msgctl(msgid, IPC_RMID, NULL);
}

通过封装好的消息队列接口实现client&service互相通信

  • service.c & client.c 需要和头文件在同一级目录下

    目录结构

    $ ls
    service.c client.c message.c message.h Makefile
    $ cat Makefile
    .PHONY:all
    all:service client
    service:service.c message.c
    	gcc $^ -o $@
    client:client.c message.c
    	gcc $^ -o $@
    	
    .PHONY:clean
    clean:
    	rm -f service client
    
//service.c
#include"./message.h"

int main()
{
  int msgid;
  char rec_msg[1024] = {0};
  char msg[1024] ={0};
  if(-1 != (msgid = CreatMsg()))
  {
    printf("CreatMsg success...\n");
  }
  if(-1 != (msgid = OpenMsg()))
  {
    printf("OpenMsg success...\n");
  } 
  while(1)
  {
    RecMsg(msgid, CLIENT_TYPE, rec_msg);
    if(0 == strcmp(rec_msg, "quit\n"))
    {
      printf("quit signal,Bye...\n");
      break;
    }
    printf("client$ %s",rec_msg);
    memset(rec_msg, 0, sizeof(rec_msg));

    printf("service# ");
    fflush(stdout);
    memset(msg, 0, sizeof(msg));
    fgets(msg, sizeof(msg), stdin);
    SendMsg(msgid, SERVICE_TYPE, msg);
  }

  DestoryMsg(msgid);
  return 0;
}
//client.c
#include"./message.h"

int main()
{
  int msgid;
  char msg[1024] = {0};
  char rec_msg[1024] = {0};
  if(-1 != (msgid = OpenMsg()))
  {
    printf("OpenMsg success...\n");
  }
  while(1)
  {
    printf("client# ");
    fflush(stdout);
    memset(msg, 0, sizeof(msg));
    fgets(msg, sizeof(msg), stdin);
    SendMsg(msgid, CLIENT_TYPE, msg);
    if(0 == strcmp(msg, "quit\n"))
    {
      printf("Bye...\n");
      break;
    }

    RecMsg(msgid, SERVICE_TYPE, rec_msg);

    printf("service# %s", rec_msg);
    memset(rec_msg, 0, sizeof(rec_msg));
  }
  return 0;
}

共享内存

  • 共享内存的本质是不同的进程看到同一块物理内存。共享内存就是先在物理内存上开辟一段空间,再通过进程的页表映射进进程的虚拟内存空间。
  • 共享内存同时映射进两个进程的虚拟内存空间,他们各自在虚拟内存空间上的地址可能不相同,但是他们的确会指向同一块物理内存
  • 共享内存是进程间通信最快的方式
  • 共享内存的生命周期跟随内核
    • ipcs -m:可以查看目前系统中存在的共享内存
    • ipcrm -m shmid:删除共享内存(通过shmid指定)
  • 共享内存没有提供互斥与同步机制,需要自己手动维护

共享内存数据结构

struct shmid_ds {
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment (bytes) */
 __kernel_time_t shm_atime; /* last attach time */
 __kernel_time_t shm_dtime; /* last detach time */
 __kernel_time_t shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current attaches */
 unsigned short shm_unused; /* compatibility */
 void *shm_unused2; /* ditto - used by DIPC */
 void *shm_unused3; /* unused */
};
  • 可以看出,共享内存也为IPC资源。

接口

//创建共享内存
#include<sys/ipc.h>
#include<sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • key:共享内存段的名字
  • size:所想要申请的共享内存的空间大小
    • 一般必须为页(1页=4KB)的整数倍
    • 如果此处size = 4097(字节),那么操作系统会分配2页(4096 * 2字节)的空间用于该共享内存,但是用户只可以使用4097字节的空间大小,这样就会造成空间的浪费
  • shmflg:由九个权限标志构成,用法与创建文件时使用的mode模式标志是一样的
  • 返回值:成功返回共享内存标识码(shmid);失败返回-1
//将创建好的共享内存传递给进程
#include<sys/types.h>
#include<sys/shm.h>

void* shmat(int shmid, const void* shmaddr, int shmflg);
  • shmid:共享内存的标识码(即为shmget成功返回值)
  • shmaddr:指定连接的地址,即指定该进程虚拟内存中的shmaddr地址与实际物理内存中的共享内存连接
    • shmaddr为NULL时,内核自动分配地址
    • shmaddr不为NULL时:
      • shmflg无SHM_RND标记,则以shmaddr为连接地址
      • shmflg有SHM_RND标记,则连接地址会自动向下调整为SHMLBA的整数倍;公式:shmaddr-(shmaddr%SHNLBA)
  • shmflg:两个可能取值为SHM_RND和SHM_RDONLY
    • shmflg = SHM_RDONLY表示连接操作用来只读共享内存
  • 返回值:成功返回一个指针,指向共享内存;失败返回-1
//将共享内存与当前进程脱离,不会删除共享内存
#include<sys/types.h>
#include<sys/shm.h>

int shmdt(void* shmaddr);
  • shmaddr:指向共享内存的指针
  • 返回值:成功返回0;失败返回-1
//删除共享内存
#include<sys/ipc.h>
#include<sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds* buf);
  • shmid:共享内存标识码
  • cmd:表示要对共享内存做的操作(有3个可取值)
    • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
    • IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
    • IPC_RMID:删除共享内存段

封装共享内存接口

//shared_mem.h
/*
 * 封装共享内存接口
 */

#ifndef _SHARED_MEM_H_
#define _SHARED_MEM_H_

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

#define PATHNAME "."
#define PRO_ID 0x6666

int CreatShm(size_t size);
int DestoryShm(int shmid);
int GetShm(int size);
void* LinkShm(int size);
int CutShm(const void* shmaddr);


#endif

//shared_mem.c
//确保头文件shared_mem.h与该文件在同一级目录下

#include"./shared_mem.h"

int CreatShm(size_t size)
{
  umask(0);
  return shmget(ftok(PATHNAME, PRO_ID), size, IPC_CREAT | IPC_EXCL | 0644);
}

int DestoryShm(int shmid)
{
  return shmctl(shmid, IPC_RMID, NULL);
}

int GetShm(int size)
{
  return shmget(ftok(PATHNAME, PRO_ID), size, IPC_CREAT);
}
void* LinkShm(int shmid)
{
  return shmat(shmid, NULL, 0);
}
int CutShm(const void* shmaddr)
{
    return shmdt(shmaddr);
}

使用封装好的接口来验证共享内存

  • 运行时service进程先运行,client后运行

    目录结构

    $ ls
    service.c client.c shared_mem.c shared_mem.h Makefile
    $ cat Makefile
    .PHONY:all
    all:service client
    service:service.c shared_mem.c
    	gcc $^ -o $@
    client:client.c shared_mem.c
    	gcc $^ -o $@
    	
    .PHONY:clean
    clean:
    	rm -f service client
    
/*
 *service.c
 *该进程用来控制共享内存的创建和销毁
 *该进程运行期间,以字符串的方式读取共享内存上的内容
 */

#include"./shared_mem.h"
int main()
{
  int shmid;
  char* shmp;
  shmid = CreatShm(1024);
  printf("%d\n", shmid);//打印共享内存在该进程的虚拟内存空间上的地址
  shmp = LinkShm(shmid);
  if(-1 == (int)shmp)
    exit(EXIT_FAILURE);
  while(1)
  {
    if(0 == strcmp(shmp, "quit\n"))
    {
      printf("quit Bye...\n");
      break;
    }
    printf("client# %s", shmp);
    fflush(stdout);
    sleep(1);

  }
 CutShm(shmp);
 DestoryShm(shmid);

  return 0;
}

/*
 *client.c
 *该进程不控制共享内存的创建与销毁,只对共享内存中的内容进行修改
 */
#include"./shared_mem.h"
int main()
{
  int shmid;
  char* shmp;
  shmid = GetShm(1024);
  shmp = LinkShm(shmid);
  printf("%x\n", shmp);
  if(-1 == (int)shmp)
    exit(EXIT_FAILURE);
  while(1)
  {
    printf("client: ");
    fflush(stdout);
    fgets(shmp, 1024, stdin);
    if(0 == strcmp(shmp, "quit\n"))
    {
      printf("Bye...\n");
      break;
    }
    sleep(1);
  }
  CutShm(shmp);
  return 0;
}

信号量集

  • 信号量集指信号量的集合,也就是由多个信号量组成的数组,可以同时控制多种资源的分配问题

信号量

  • 信号量:本质上是一个计数器
  • 信号量本身也是临界资源。信号量的++(V操作;释放操作)/–(P操作;申请操作)操作一定是原子的
  • 共享资源必须是互斥的,这样才能保证共享资源的原子性
  • 两个进程看到的一份公共资源我们称之为临界资源,把各个进程中访问临界资源的代码叫做临界区
  • 互斥:在临界区当中,通过进程临界区访问临界资源时,任何时候只能有一个进程访问临界资源
  • 同步:在互斥的基础之上,让访问具有一定的顺序
  • 原子性:一件事情只有两种状态

同步与互斥

互斥
  • 互斥即就是当一个进程在访问一个共享资源时,其他的进程就不可以访问该共享资源,也就是说任意时刻,只有一个进程可以访问该共享资源
同步
  • 进程同步代表多个进程要相互合作完成同一个任务时,通常会有多个进程访问同一份资源,也就是在互斥的基础上,让访问具有一定顺序

信号量结构体伪代码

struct semaphore
{
    int value;
    pointer_PCB queue;
};
//信号量本质上是一个计数器

P

P(s)
{
    s.value = s.value - 1;
    if(s.value < 0)
    {
        //1.将该进程的状态设置为等待状态
        //2.将该进程的PCB插入相应的等待状态中的PCB队列的队尾
    }
}

V

V(s)
{
    s.value = s.value + 1;
    if(s.value <= 0)
    {
        //1.唤醒相应的等待状态PCB队列中的一个进程
        //2.将该进程的状态改变为就绪态
        //3.将该进程插入就绪状态PCB队列的队尾
    }
}

信号量集结构体

struct semid_ds
{
    struct ipc_perm sem_perm; /* Ownership and permissions */
    time_t         sem_otime; /* Last semop time */
    time_t         sem_ctime; /*Last change time */
    unsigned short sem_nsems; /* No. of semaphores in set */
};

接口

//用来创建和访问一个信号量集
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • key:信号量集的名字
  • nsems:信号集中包含信号量的个数
  • semflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
  • 返回值:成功返回信号量集的标识码(非负整数;semid);失败返回-1
//用来控制信号量集
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
  • semid:信号量集标识码

  • semnum:信号集中信号量的序号

  • cmd:要执行的操作

    • 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) */
        };
        
    • GETVAL:获取信号量集中的信号量计数值

    • IPC_STAT:把semid_ds结构体中的数据设置为信号量集的当前关联值

    • IPC_SET:在进程有足够权限的前题下,将信号量集的当前关联值设置为semid_ds数据结构中给出的值

    • IPC_RMID:删除信号量集

  • 最后一个参数根据命令的不同而不同

  • 返回值:成功返回0;失败返回-1

//用来操作一个信号量集中的信号量
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned nsops);
  • semid:要访问的信号量集的标识码

  • sops:是一个指针,指向一个结构体,该结构体操作系统已经定义

    • struct sembuf
      {
          short sem_num;
          short sem_op;
          short sem_flg;
      };
      //sem_num:信号量的编号
      //sem_op:是信号量一次PV操作时加减的数值,一般会用到两个值:
      //		  一个是‘-1’,也就是P操作,等待信号量变得可用;
      //		  另一个是‘+1’,也就是V操作,发出信号量已经变得可用;
      //sem_flg:有两个取值,分别为IPC_NOWAIT和IPC_UNDO
      
  • nsops:信号量的个数

  • 返回值:成功返回0;失败返回-1

使用信号量集

  1. 获取key值,建议使用ftok()函数生成
  2. 创建/获取信号量集
  3. 初始化信号量集
  4. 操作信号量集
  5. 如果不在使用信号量集,则删除信号量集

封装接口,并采用二元信号量进行测试

  • 文件结构

    $ ls
    Makefile my_sem.c my_sem.h test.c
    $ cat Makefile
    test:test.c my_sec.c
    	gcc $^ -o $@
    .PHONY:clean
    	rm -f test
    
    //my_sem.h
    /*
     * 封装二元信号量集
     */
    #ifndef _MY_SEM_H_
    #define _MY_SEM_H_
    
    #include<stdio.h>
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/sem.h>
    #include<unistd.h>
    #include<stdlib.h>
    #include<wait.h>
    
    
    //为ftok函数定义
    #define PATHNAME "."
    #define PRO_ID 0x6666
    
    union semnu
    {
      int val;
      struct semid_ds * buf;
      unsigned short *array;
      struct seminfo *_buf;
    };
    
    int CreatSem(int nums);
    int CommSem(int nums, int semflg);
    int InitSem(int semid, int nums, int initval);
    int GetSem(int nums);
    int DestorySem(int semid);
    int PVcomm(int semid, int who, int op);
    int P(int semid);
    int V(int semid);
    #endif
    
    
    //my_sem.c
    #include"./my_sem.h"
    
    int CommSem(int nums, int semflg)
    {
      key_t key = ftok(PATHNAME, PRO_ID);
      if(key < 0)
      {
        exit(EXIT_FAILURE);
      }
    
      return semget(key, nums, semflg);
    }
    
    //创建信号量集
    int CreatSem(int nums)
    {
      return CommSem(nums, IPC_CREAT|IPC_EXCL|0644);
    }
    
    //得到已创建的信号量集
    int GetSem(int nums)
    {
      return CommSem(nums, IPC_CREAT);
    }
    
    
    int PVcomm(int semid, int who, int op)
    {
      struct sembuf _sembuf;
      _sembuf.sem_num = who;
      _sembuf.sem_op = op;
      _sembuf.sem_flg = 0;
    
      return semop(semid, &_sembuf, 1);
    }
    
    //对信号量集进行P操作,该处即为给信号量的计数值-1
    int P(int semid)
    {
      return PVcomm(semid, 0, -1);
    }
    
    //对信号量进行V操作,该处即为给信号量的计数值+1
    int V(int semid)
    {
      return PVcomm(semid, 0, +1);
    }
    
    //删除信号量集
    int DestorySem(int semid)
    {
      return semctl(semid, 0, IPC_RMID);
    }
    
    
    //初始化信号量集,二元信号量集初始信号量计数值应该初始化为1
    int InitSem(int semid, int nums, int initval)
    {
      union semnu _semnu;
      _semnu.val = initval;
      return semctl(semid, nums, SETVAL, _semnu);
    }
    
    
    //test.c(不添加二元信号量)
    #include"./my_sem.h"
    int main()
    {
      //int semid = CreatSem(1);//创建包含一个信号量的信号集
      //InitSem(semid, 0, 1);//初始化所创建的信号集,这里用1初始化是因为第一次使用信号量时,他所对应的对象应该是可以访问的
      pid_t id = fork();
      if(-1 == id)
      {
        exit(EXIT_FAILURE);
      }
      else if(0 == id)
      {
        int i =1000;
        while(i--)
        {
          //P(semid);//子进程对信号量集进行P操作
          printf("A");
          fflush(stdout);
          usleep(12345);
          printf("A ");
          fflush(stdout);
          usleep(12378);
          //V(semid);//子进程对信号量集合进行V操作
        }
      }
      else
      {
        int i = 1000;
        while(i--)
        {
          //P(semid);//父进程对信号量集进行P操作
          printf("B");
          fflush(stdout);
          usleep(13456);
          printf("B ");
          fflush(stdout);
          usleep(13487);
          //V(semid);//父进程对信号量集进行V操作
        }
    
    
        while(wait(NULL) != id);//等待子进程退出
        DestorySem(semid);//销毁信号量集
      }
      return 0;
    }
    
  • 运行结果:iomOaT.png

//添加二元信号量
//因为这里的显示器即为子进程与父进程的共享资源,所以我们使用二元信号量,当子进程使用显示器进行P操作时,父进程就必须等待子进程进行V操作之后才可以向下执行
#include"./my_sem.h"
int main()
{
  int semid = CreatSem(1);//创建包含一个信号量的信号集
  InitSem(semid, 0, 1);//初始化所创建的信号集,这里用1初始化是因为第一次使用信号量时,他所对应的对象应该是可以访问的
  pid_t id = fork();
  if(-1 == id)
  {
    exit(EXIT_FAILURE);
  }
  else if(0 == id)
  {
    int i =1000;
    while(i--)
    {
      P(semid);//子进程对信号量集进行P操作
      printf("A");
      fflush(stdout);
      usleep(12345);
      printf("A ");
      fflush(stdout);
      usleep(12378);
      V(semid);//子进程对信号量集合进行V操作
    }
  }
  else
  {
    int i = 1000;
    while(i--)
    {
      P(semid);//父进程对信号量集进行P操作
      printf("B");
      fflush(stdout);
      usleep(13456);
      printf("B ");
      fflush(stdout);
      usleep(13487);
      V(semid);//父进程对信号量集进行V操作
    }


    while(wait(NULL) != id);//等待子进程退出
    DestorySem(semid);//销毁信号量集
  }
  return 0;
}

  • 运行结果:ionFZ6.png

猜你喜欢

转载自blog.csdn.net/h___q/article/details/83686537