linux进程间的通信(IPC)

文摘   2024-10-21 16:01   陕西  


IPC是各种进程通信方式的统称。IPC 单机形式有管道(无名管道 和 命名管道(FIFO))、消息队列、共享内存、信号、信号量多机形式有socket、streams其中Socket和Streams支持不同主机上的两个进程IPC。


一、管道

管道,通常指无名管道,是UNIX系统IPC最古老的形式。


1.无名管道


1.1特点

    1、工作方式一般使用半双工(即数据只能在一个方向上流动),保留一组读写端(只能读或写,读的时候要把写关上,反之一样)。

    2、使用只限于具有亲缘关系(也是父子进程或兄弟进程之间)的进程之间。

    3、使用管道可以理解为一种特殊的文件,可以使用read、write等函数进行读写(lseek不可用), 但它不是普通文件,存在于内存中

    4、数据被读取后就消失(类似于水管),不可进行二次读取


1.2管道工作原理

 1.3函数原型

#include <unistd.h>int pipe(int pipefd[2]);

参数说明:

pipe创建一个管道,会生成两个管道描述符:

pipefd[0]:从管道进行读取数据。

pipefd[1]:向管道进行写入数据。


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


1.4编程实现

读取数据时要先关闭写,写入数据时要先关闭读,只能单向。

#include <unistd.h>#include <stdio.h>#include <string.h>
int main(){
// int pipe(int pipefd[2]); int fd[2]; if(pipe(fd)==-1) {    printf("create pipe failed\n"); } int pid; char readbuff[128]={0}; char readbuff2[128]={0}; pid=fork();
if(pid<0) {      printf("create child process\n"); } else if(pid==0) { printf("this is child\n"); close(fd[1]); read(fd[0],readbuff,sizeof(readbuff)); read(fd[0],readbuff2,sizeof(readbuff2));      close(fd[0]); printf("from father process:%s,2:%s\n",readbuff,readbuff2); exit(0); } else{      sleep(3); printf("this is father\n"); close(fd[0]); write(fd[1],"hello this is father",strlen("hello this is father"));     close(fd[1]);     wait(NULL);     exit(0);   } return 0;
}

运行结果:


从运行结果看,数据被读取后就消失(类似于水管),不可进行二次读取。


2.命名管道(有名管道)


命名管道也被称为FIFO文件,是一种特殊的文件。由于linux所有的事物都可以被视为文件,所以对命名管道的使用也就变得与文件操作非常统一,可以用open、write、read等函数操作。


2.1特点:

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

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


2.2函数原型:


查看man手册:

man 3 mkfifo
 #include <sys/types.h> #include <sys/stat.h>
 int mkfifo(const char *pathname, mode_t mode);

参数说明:

     pathname:创建管道的路径文件名;

     mode:管道创建的模式,与open函数中的mode一样。


返回值:执行成功时,返回0,失败则返回-1。


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

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

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


2.3编程实现


先测试没有指定O_NONBLOCK,代码如下:

mkfifo.c文件

#include <unistd.h>#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <errno.h>
int main(){
if(mkfifo("./fifo.txt",0600)==-1 && errno!=EEXIST) { printf("mkfifo failed\n"); perror("why");
}  int fd=open("./fifo.txt",O_RDONLY);  //只读 printf("open success\n"); close(fd); return 0;
}

运行该文件会出现阻塞,因为用只读open打开此fifo文件,需其他进程用写来打开此fifo文件才不会阻塞。

fifowrite.c

#include <unistd.h>#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <errno.h>#include <stdlib.h>int main(){
int fd=open("./fifo.txt",O_WRONLY); if(fd==-1) { printf("open failed\n"); exit(0); }  printf("open success\n"); close(fd); return 0;
}

先运行mkfifo.c文件,再打开一个终端去运行fifowrite.c文件。

运行结果:


fifowrite.c文件是没有发生阻塞的,因为先运行mkfifo.c,open是指定只读的。


2.4实现两个进程通信

在上面代码的基础上进行改进。

read.c

#include <unistd.h>#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <errno.h>#include <stdlib.h>
int main(){ char readbuff[128]={0}; if(mkfifo("./fifo.txt",0600)==-1 && errno!=EEXIST) { printf("mkfifo failed\n"); perror("why");
}
int fd=open("./fifo.txt",O_RDONLY); if(fd==-1) { printf("open failed\n"); exit(0); } int nread=read(fd,readbuff,sizeof(readbuff)); printf("read %d byte,content:%s\n",nread,readbuff); close(fd); return 0;
}

write.c

#include <unistd.h>#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <errno.h>#include <stdlib.h>int main(){
int fd=open("./fifo.txt",O_WRONLY); if(fd==-1) { printf("open failed\n"); exit(0); } write(fd,"hello,my from write",strlen("hello,my from write")); printf("write success\n"); close(fd); return 0;
}

运行结果:


不能使用lseek函数,可以试一下。


指定O_NONBLOCK,以O_RDONLY为例,在O_RDONLY | O_NONBLOCK,可以自己进行测试。


如果open用可读可写的方式打开此FIFO文件,则不会阻塞。


以write.c为例

#include <unistd.h>#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <errno.h>#include <stdlib.h>int main(){
int fd=open("./fifo.txt",O_RDWR); if(fd==-1) { printf("open failed\n"); exit(0); } //write(fd,"hello,my from write",strlen("hello,my from write")); printf("write success\n"); close(fd); return 0;
}

运行结果:




最后说明一下,无论是无名管道还是命名管道,数据读取后都会消失


二、消息队列


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


2.1、特点

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

    2、消息队列独立于发送与接受进程,进程终止时,消息队列不会被删除,但消息内容读取完之后,内容(消息)会被删除;

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


2.2工作原理


2.3函数原型

 #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h>
//创建或打开消息队列:成功返回队列ID,失败返回-1 int msgget(key_t key, int msgflg); //添加消息:成功返回0,失败返回-1 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//读取消息:成功返回消息数据的长度,失败返回-1 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);//控制消息队列:成功返回0,失败返回-1 int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数说明:


在以下两种情况下,msgget将创建一个新的消息队列:

  (1).如果没有与键值key相对应的消息对列,并且flag中包含了IPC_CREAT标志位。

  (2).key参数为IPC_PRIVATE


补充一下:键值不一样,msgget函数返回值ID号不一样。ID号从0开始。


函数msgrcv在读取消息队列时,type参数有下面几种情况:

  (1).type==0,返回队列中的第一个消息;

  (2).type>0,返回队列中消息类型为type的第一个消息;

  (3).type<0,返回队列中消息类型值小于或等于type绝对值的消息,如果有多个,则取类型值最小的消息。


从这可以看出,type值非0时用于以非先进先出次序读消息。也可以把type看作优先级的权值。


msgid:msgget函数返回值;

msgflg:默认值为0。


2.4编程实现收发数据

msgread.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(){
int msgid;
struct msgbuf readbuff;
struct msgbuf buff={98,"my rcv!"};
msgid=msgget(0x1234,IPC_CREAT|0777); printf("msgid=%d\n",msgid); if(msgid==-1) { printf("get queue fail\n"); } msgrcv(msgid,&readbuff,sizeof(readbuff.mtext),888,0);
msgsnd(msgid,&buff,strlen(buff.mtext),0);       msgctl(msgid,IPC_RMID,NULL);//删除消息队列       printf("read from you:%s\n",readbuff.mtext); return 0;}

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(){
int msgid;
struct msgbuf buff={888,"hello my from msgSend"};
struct msgbuf readbuff;       msgid=msgget(0x1234,0); printf("msgid=%d\n",msgid); if(msgid==-1) { printf("get queue fail\n"); } msgsnd(msgid,&buff,strlen(buff.mtext),0); printf("send success\n");
msgrcv(msgid,&readbuff,sizeof(readbuff.mtext),98,0);
printf("read from you:%s\n",readbuff.mtext); return 0;}

运行结果:


键值key是直接给定的,key键值也可以用ftok函数进行获取。


2.5 ftok函数


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


函数原型:

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok( const char * fname, int id )


参数说明:

fname就是你指定的文件名(已经存在的文件名),一般使用当前目录。
如:
key_t key;
key = ftok(".", 1); 这样就是将fname设为当前目录。

id是子序号。虽然是int类型,但是只使用8bits(1-255)。

在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
查询文件索引节点号的方法是: ls -ai

索引节点是指在许多类Unix文件系统中的一种数据结构。每个索引节点保存了文件系统中的一个文件系统对象的元信息数据,但不包括数据内容或者文件名。
2.6用ftok进行编程
 //key通过ftok函数获得 key_t key; key=ftok(".",2); printf("key=%x\n",key); msgid=msgget(key,IPC_CREAT|0777);

三、共享内存

3.1临界资源和临界区

临界资源:各进程采用互斥的方式,实现共享的资源。

临界资源是一次仅允许一个进程使用的共享资源。


临界区:每个进程中访问临界资源的代码称为临界区。


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


3.2函数原型

 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/shm.h> //创建或获取一个共享内存:成功返回共享内存ID,失败返回-1int shmget(key_t key, size_t size, int shmflg);//连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1void *shmat(int shmid, const void *shmaddr, int shmflg);//断开与共享内存的连接:成功返回0,失败返回-1int shmdt(const void *shmaddr);//控制共享内存的相关信息:成功返回0,失败返回-1int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

shmget函数中参数:

key:通过ftok函数获得;

size:创建共享内存的大小,必须写;

shmflg:和消息对列msgflag用法一样;


shmat函数中参数:

shmid:shmget函数返回的内存id;

shmaddr:设置值为NULL,系统会选择适合的页地址去绑定这段地址;

shmflg:如果shmaddr不设置NULL,需设置shmflag,否则设置值为0;


shmdt函数中参数:

shmaddr:shmat函数返回的内存地址。



3.3编程实现共享内存数据的收发


shmw.c

#include <stdio.h>#include <sys/types.h>#include <sys/ipc.h>#include <string.h>#include <sys/sem.h>#include <sys/shm.h>#include <stdlib.h>
int main(){
int shmid;
char *shmaddr;
key_t key; key=ftok(".",2);
shmid=shmget(key,1024*4,IPC_CREAT|0666); if(shmid==-1) { printf("shmget fail\n"); exit(0); }
shmaddr=shmat(shmid,NULL,0);
strcpy(shmaddr,"my de name!");
shmdt(shmaddr);
printf("quit\n"); return 0;}

shmr.c

#include <stdio.h>#include <sys/types.h>#include <sys/ipc.h>#include <string.h>#include <sys/sem.h>#include <sys/shm.h>#include <stdlib.h>
int main(){
int shmid;
char *shmaddr;
key_t key; key=ftok(".",2);
shmid=shmget(key,1024*4,0); if(shmid==-1) { printf("shmget fail\n"); exit(0); }
shmaddr=shmat(shmid,NULL,0);
printf("shmat ok\n"); printf("datas:%s\n",shmaddr);
shmdt(shmaddr);       shmctl(shmid,IPC_RMID,0);  //读取数据后进行删除共享内存      printf("quit\n"); return 0;}

运行结果:

读取数据:


写入:


可以通过ipcs -m查看共享内存,ipcrm -m shmid可以删除共享内存。


每次向共享内存写入新的数据时,原先的数据会被清除掉,但共享内存中数据在读取之后仍然存在除非共享内存删除。


四、信号

      对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件的方法。


4.1概述

1、信号的名字和编号
   每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGQUIT ”、“SIGKILL”、“SIGINT”等等。
   信号定义在signal.h头文件中,信号名都定义为正整数。具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0y有特殊的应用。

信号的名称如下:


2.信号的处理:
信号的处理有三种方法,分别是:
忽略、捕捉和默认动作。

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


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

  3. 系统默认动作:对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。


常用在终端使用kill命令进行终止进程,如kill -9 进程ID 或kill -SIGKILL 进程ID


3.信号处理函数的注册
信号处理函数的注册不只一种方法,分为入门版和高级版
  1. 入门版:函数signal
  2. 高级版:函数sigaction

4.信号处理函数的发送

信号发送函数也不止一个,同样分为入门版和高级版
1.入门版:
kill
2.高级版:sigqueue

4.2入门版 :signal函数:


函数原型:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
理解typedef void (*sighandler_t)(int)语句:
  例如语句 typedef int * a; 理解它的正确步骤是这样的:先别看typedef,就剩下int * a; 这个语句再简单不过,就是声明了一个指向整型变量的指针a (注意:定义只是一种特殊的声明),加上typedef之后就解释成声明了一种指向整型变量指针的类型a 。所以sighandler_t变成了一种类型,指向一个函数接受一个整形变量并返回一个无类型指针

参数说明:
signum:等待信号产生的信号编号。
handler:函数结构void (*sighandler_t)(int),当信号产生时调用该函数接受发送的信号值

4.3编程实现信号的发送与接受
signal.c
#include <signal.h>#include <stdio.h>

void handler(int signum){
printf("get signum:%d\n",signum);
switch(signum) { case 2:        printf("SIGINT\n");break;  //就是ctrl+c 产生的 case 9: printf("SIGKILL");break; case 10: printf("SIGUSR1\n");break;
}}
int main(){ signal(SIGINT,handler); signal(SIGKILL,handler); signal(SIGUSR1,handler);
while(1); return 0;
}

发送信号有两种:一种是在终端输入kill命令,另一种通过发送函数kill进行发送命令。

运行结果

方式-:


方式二,kill函数:

 函数原型:

#include <sys/types.h>#include <signal.h>
//成功执行返回0,失败返回-1int kill(pid_t pid, int sig);

参数说明:

pid:接受信号进程的pid号。

sig:发送接受信号值。


sigkill.c

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

int main(int argc,char **argv){ int signum; int pid;   if(argc!=3)  {   printf("param error\n");   exit(0);  } signum=atoi(argv[1]); pid=atoi(argv[2]); printf("signum=%d,pid=%d\n",signum,pid);
kill(pid,signum);
return 0;
}


对于已注册的信号,使用 kill 发送都可以正常接收到,但是如果发送了未注册的信号,则会使得应用程序终止进程。


4.4高级版:sigaction函数


我们已经实现信号的收发,总感觉缺少点什么。所以要使用高级版的,在发送信号的同时携带一些数据。


函数原型:

 #include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数说明:

signum等待信号产生的信号编号。

act:是一个结构体类型,如果不为空说明需要对该信号有新的配置。

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

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);           };

如果sa_flags设置为SA_SIGINFO说明了信号处理程序带有附加信息,也就是会调用 void     (*sa_sigaction)(int, siginfo_t *, void *);。否则,系统会默认使用 void     (*sa_handler)(int);。在此,还要特别说明一下,sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。


siginfo_t 是一个结构体,说明一下:

   //信号处理函数    void handler(int sig, siginfo_t *info, void *ucontext){               ...}
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 */ union sigval 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 */ long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */ int si_fd; /* File descriptor */ short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */ void *si_lower; /* Lower bound when address violation occurred (since Linux 3.19) */ void *si_upper; /* Upper bound when address violation occurred (since Linux 3.19) */ int si_pkey; /* Protection key on PTE that caused fault (since Linux 4.6) */ void *si_call_addr; /* Address of system call instruction (since Linux 3.5) */ int si_syscall; /* Number of attempted system call (since Linux 3.5) */ unsigned int si_arch; /* Architecture of attempted system call (since Linux 3.5) */ }

可以看到结构体成员很多,si_signo 和 si_code 是必须实现的两个成员。可以通过这个结构体获取到信号的相关信息。


关于发送来的数据,保存在si_int和*si_ptr中,和si_value成员中sival_int和*sival_ptr保存的数据是一样。


在接收数据时,ucontext要保证是非空的,然后打印数据。

union sigval结构体

union sigval {               int   sival_int;               void *sival_ptr;           };

4.5信号发送函数高级版:sigqueue函数

函数原型:

#include <signal.h>

//成功执行返回0,失败返回-1int sigqueue(pid_t pid, int sig, const union sigval value);

参数说明:

pid:注册信号进程的pid号。

sig:所要发送的信号编号。

value:发送信号时所要携带的数据。


4.6编程实现高级版函数信号的发送与接受

接受信号sigaction.c

#include <signal.h>#include <stdio.h>#include <sys/types.h>#include <unistd.h>//int sigaction(int signum, const struct sigaction *act,   //                  struct sigaction *oldact);
void handler(int signum,siginfo_t *info,void *context){ printf("signum=%d\n",signum); if(context!=NULL) { printf("get data:%d\n",info->si_int); printf("get data:%d\n",info->si_value.sival_int); printf("send process pid;%d\n",info->si_pid); }}
int main(){
struct sigaction act;
printf("my pid=%d\n",getpid()); act.sa_sigaction=handler; act.sa_flags=SA_SIGINFO;//sa_sigaction should be set instead of sa_handler sigaction(SIGUSR1,&act,NULL);
while(1); return 0;}


发送信号sigqueue.c

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

int main(int argc,char **argv){ int signum; int pid; union sigval value;if(argc!=3) { printf("param error\n"); exit(0); }  signum=atoi(argv[1]);  //字符转整型 pid=atoi(argv[2]); printf("signum=%d,pid=%d\n",signum,pid); value.sival_int=100;
sigqueue(pid,signum,value); return 0;
}

运行结果:


通过结果可以看到,si_int中的数据和si_value成员中sival_int数据保存是一样的。


五、信号量


5.1概述


  1. 信号量(Semaphore)是一个计数器信号量用于实现进程间的互斥与同步,而不是用于存储进程间的通信数据。

  2. 信号量是由操作系统来维护的,用户进程只能通过初始化和两个标准原语(P、V原语)来访问。信号量初始化可指定一个非负整数,及空闲资源总数。

P原语:P是荷兰语Proberen(测试)的首字母。为阻塞原语,负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞。

V原语:V是荷兰语Verhogen(增加)的首字母。为唤醒原语,负责把一个被阻塞的进程唤醒。操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒。


5.2特点

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

         2.信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作;

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

          4.支持信号量组。


5.3函数原型

#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>
//创建或获取一个信号量集,成功执行返回对应ID号,失败则返回-1int semget(key_t key, int nsems, int semflg);//控制信号量的相关信息,成功执行返回一个非负整数值,失败返回-1int semctl(int semid, int semnum, int cmd, ...);//改变信号量的值操作函数,成功执行返回0,失败返回-1int semop(int semid, struct sembuf *sops, size_t nsops);

参数说明:

semget函数:

    key:通过ftok函数获取的键值;

    nsems:一个信号量集中信号量的个数;

    semflg:和消息队列中msgget函数中msgflg参数用法一样。


semctl函数:

    semid:semget函数返回的ID号;

    semnum:信号量集中对信号量进行编号,一般信号量从0开始编号;


这个函数可以有3个或4个参数,取决于cmd。当有4个参数时,这第4个参数必须指定union semun这个类型,而且必须在程序中定义这个结构体。结构体如下:

 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) */           };

在本次编程实验中只使用了结构体第一个参数val来设置初始信号量值,使用第一个参数,cmd指定为SETVAL。


semop函数:

     semid:semget函数返回的ID号;

     sops:是一个struct sembuf结构体类型指针变量,一般发送多个数据写为数组;

     nsops:需要发送的sops数组中元素个数。


struct sembuf结构体包含的成员如下:

unsigned short sem_num;  /* semaphore number */  //信号量编号short          sem_op;   /* semaphore operation */short          sem_flg;  /* operation flags */

sem_num:信号量编号。

在信号量集中,第一个信号量编号为0,sem_op可以有三种值:

        sem_op==0:进程必须对信号量集具有读取权限。这是一个“等待零”操作:如果semval为零,则操作可以立即继续(不发生阻塞等待)否则,如果在sem_flg中指定了IPC_NOWAIT,semop()失败。否则,进程就会出现阻塞等待,直到semval变为0。


        sem_op>0该操作将此值添加到信号量值(semval)中。此外,如果为此操作指定了SEM_UNDO,系统将从该信号量的信号调整(semadj)值中减去值sem_op。


        sem_op<0:如果semval值大于或等于这个sem_op绝对值,这个操作能立即继续:从semval中减去semop的绝对值。如果为此操作指定了SEM_UNDO,则系统会将sem_op的绝对值添加到此信号量的信号量调整(semadj)值中。否则如果在sem_flg中指定了IPC_NOWAIT,semop()失败。否则就阻塞等待,直到semval值大于或等于这个sem_op绝对值


简单的记一下:

      sem_op==0如果semval为零,当前进程不再等待,也就是申请到了一个空间资源;

      sem_op>0:释放空间资源;

      sem_op<0:申请空间资源。


sem_flg可以指定两个宏:IPC_NOWAIT和SEM_UNDO。如果设置为SEM_UNDO,当进程退出时将会立即取消操作。

       


5.4编程实现信号量PV操作

#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <stdio.h>//       int semget(key_t key, int nsems, int semflg);#include <unistd.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 (Linux-specific) */ };
void PGetKey(int id){ struct sembuf set;
set.sem_num=0; set.sem_op=-1; set.sem_flg=SEM_UNDO; semop(id,&set,1);
printf("getkey\n");
}

void VPutKey(int id){  struct sembuf set;  set.sem_num=0;  set.sem_op=1;  set.sem_flg=SEM_UNDO;  semop(id,&set,1);
printf("VPut back the key\n");
}
int main(){ key_t key; int semid; key=ftok(".",1);

semid=semget(key,1,IPC_CREAT|0666); union semun initsem; initsem.val=0;
semctl(semid,0,SETVAL,initsem);
int pid=fork();if(pid>0){ PGetKey(semid); printf("this is father\n"); VPutKey(semid);}else if(pid==0){ printf("this is child\n"); VPutKey(semid);}else{ printf(" fork fail\n");} return 0;}

运行结果:


可通过ipcs命令查看消息队列、共享内存、信号量。ipcs -s可以直接查看信号量。


5.5进一步实现PV操作一直运行,并且保证子进程优先运行

 比上面代码多了PInitKey函数,main有所改变。

void PInitKey(int id){  struct sembuf set1;  int sd;  set1.sem_num=0;  set1.sem_op=0;  set1.sem_flg=SEM_UNDO;
sd=semop(id,&set1,1); if(sd==-1) { printf("semop error\n"); exit(0); } printf("getInitkey\n");
}
int main(){ key_t key; int semid; key=ftok(".",1);

semid=semget(key,1,IPC_CREAT|0666); union semun initsem; initsem.val=0;
semctl(semid,0,SETVAL,initsem);
int pid=fork();if(pid>0){ while(1) { PGetKey(semid); printf("this is father\n"); VPutKey(semid); semctl(semid,0,SETVAL,initsem); sleep(2);}}else if(pid==0){ while(1) { PInitKey(semid); printf("initsem.val=%d\n",initsem.val); printf("this is child\n"); VPutKey(semid); sleep(2);}}else{ printf(" fork fail\n");} return 0;}

运行结果:


六、生产者——消费者例子


生产者在缓冲区生产一个件商品,通知消费者从缓冲区消耗一件商品,消费者消耗完,通知生产者生产下一个商品。


    • 使用共享内存充当缓冲区,放入数据。

    • 通过消息队列来通知生产者和消费者。

    • 利用信号量控制着进程同步。


代码如下:

producer.c

#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/msg.h>#include <sys/shm.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 (Linux-specific) */ };

//P 申请一个空间资源void PGetSem(int id){ struct sembuf set1; int sd; set1.sem_num=0; set1.sem_op=-1; set1.sem_flg=SEM_UNDO;
sd=semop(id,&set1,1); if(sd==-1) { printf("semop error\n"); exit(0); } printf("PGet\n");
}
//V 释放一个空间资源void VPutSem(int id){ struct sembuf set;
set.sem_num=0; set.sem_op=1; set.sem_flg=SEM_UNDO; semop(id,&set,1);
printf("VPut\n");
}
struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[128]; /* message data */ };

int main(){ key_t key; int semid,shmid,msgid;
union semun initsem; struct msgbuf rbuff; struct msgbuf buff;
char *shmaddr; char cmd; char flag=1;
key=ftok(".",3); initsem.val=1; //设置信号量初始值 buff.mtype=10; //创建消息队列 msgid=msgget(key,IPC_CREAT|0666); if(msgid==-1) { printf("create msgQueue error\n"); exit(0); }
//创建共享内存 shmid=shmget(key,1024*2,IPC_CREAT|0666);  if(shmid==-1) { printf("create shm error\n"); exit(0); }
//创建信号量集 semid=semget(key,1,IPC_CREAT|0666); if(semid==-1) { printf("create sem error\n"); exit(0); } //控制信号量 semctl(semid,0,SETVAL,initsem); while(flag) {    printf("Please input a cmd:"); scanf("%c",&cmd); getchar(); printf("\n"); buff.mtext[0]=cmd; switch(cmd) { case 'r':             PGetSem(semid);     //P操作             shmaddr=shmat(shmid,NULL,0); //往缓冲区生产一个商品 strcpy(shmaddr,"I produce one thing!");             printf("Write OK\n");  VPutSem(semid); //V操作 //通知消费者消耗该商品 msgsnd(msgid,&buff,sizeof(struct msgbuf),0); printf("msg send OK\n");             //等待消费者消耗该商品 msgrcv(msgid,&rbuff,sizeof(struct msgbuf),20,0); printf("msg from consumer:%ld,%s\n",rbuff.mtype,rbuff.mtext); printf("msg read OK\n");      memset(rbuff.mtext,0,128);       printf("\n\n");break; case 'q': msgsnd(msgid,&buff,sizeof(struct msgbuf),0); printf("quit\n"); flag=0;break; default: printf("input error\n"); } }    //断开与共享内存连接 shmdt(shmaddr);    //删除消息队列、共享内存、信号量 msgctl(msgid,IPC_RMID,NULL); shmctl(shmid,IPC_RMID,NULL); semctl(semid,0,IPC_RMID,NULL);     return 0;}


consumer.c

#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <sys/msg.h>#include <sys/shm.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 (Linux-specific) */ };

//P 申请一个空间资源void PGetSem(int id){ struct sembuf set1; int sd; set1.sem_num=0; set1.sem_op=-1; set1.sem_flg=SEM_UNDO;
sd=semop(id,&set1,1); if(sd==-1) { printf("semop error\n"); exit(0); } printf("PGet\n");
}
//V 释放一个空间资源void VPutSem(int id){ struct sembuf set;
set.sem_num=0; set.sem_op=1; set.sem_flg=SEM_UNDO; semop(id,&set,1);
printf("VPut\n");
}
struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[128]; /* message data */ };int main(){ key_t key; int semid,shmid,msgid; struct msgbuf rbuff; struct msgbuf wbuff={20,"I have taked"}; char *shmaddr;

key=ftok(".",3);
  //获取消息队列 msgid=msgget(key,0); if(msgid==-1) { printf("create msgQueue error\n"); exit(0); } //获取共享内存 shmid=shmget(key,1024*2,0); if(shmid==-1) { printf("create shm error\n"); exit(0); }
  //获取信号量集  semid=semget(key,1,0); if(semid==-1) { printf("create sem error\n"); exit(0); }
while(1)     {      //接收生产者发来的通知消息          msgrcv(msgid,&rbuff,sizeof(struct msgbuf),10,0); if(rbuff.mtext[0]=='r') //消耗商品 {            PGetSem(semid);        //P操作 shmaddr=shmat(shmid,NULL,0);//拿走商品            printf("data from producer:%s\n",shmaddr);  printf("data read OK\n");            VPutSem(semid);       //V操作            //通知生产者缓冲区商品已拿走,生产下一个商品 msgsnd(msgid,&wbuff,sizeof(struct msgbuf),0); printf("msg send OK\n"); printf("\n\n"); } if(rbuff.mtext[0]=='q') //退出 { printf("quit\n"); break; } memset(rbuff.mtext,0,128); }    //断开与共享内存的连接 shmdt(shmaddr);     return 0;}


Linux内核之旅
Linux内核之旅
 最新文章