目录
在Linux下的多个进程间的通信机制叫做IPC,它是多个进程之间相互沟通的一种方法。在Linux下有多种进程间通信的方法:半双工管道、FIFO (命名管道)、消息队列、信号量、共享内存等。使用这些通信机制可以为Linux下的网络服务器开发提供灵活而又坚固的框架。
匿名管道
管道是种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的办法,自UNIX操作系统诞生,管道就存在了。
1.基本概念
由于管道仅仅是将某个进程的输出和另一个进程的输入相连接的单向通信的办法,因此称其为“半双工”。在shell中管道用“ | ”表示。
ls -l | grep *.c
把ls -l的输出当做“grep *.c”的输入,管道在前一个进程中建立输入通道,在后一个进程建立输出通道,将数据从管道的左边传输到管道的右边将ls -l的输出通过管道传给“grep *.c”。
进程创建管道,每次创建两个文件描述符来操作管道。其中一个对管道进行写操作,另一个描述符对管道进行读操作。管道将两个进程通过内核连接起来,两个文件描述符连接在一起。如果进程通过管道fda[0]发送数据,它可以从fdb[0]获得信息。
由于进程A和进程B都能够访问管道的两个描述符,因此管道创建完毕后要设置在各个进程中的方向,希望数据向那个方向传输。这需要做好规划,两个进程都要做统一的设置,在进程A中设置为读的管道描述符,在进程B中要设置为写;反之亦然,并且要把不关心的管道端关掉。对管道的读写与一般的I0系统函数一致, 使用write()函数写入数据,read()函数读出数据,某些特定的IO操作管道是不支持的,例如偏移函数lseek()。
创建管道---pipe()
#include <unistd.h>
int pipe(int fd[2]);
fd是一个文件描述符的数组,用于保存管道返回的两个文件描述符。数组中的第1个元素(下标为0)是为了读操作而打开的,而第2个元素(下标为1),是为了写操作而创建和打开的。(0看作一张嘴,用来读,1看作一支笔,用来写)。当函数执行成功时返回0,失败返回错误代码。
只建立管道看起来没有什么用处,要使管道有切实的用处,需要与进程的创建结合起来,利用两个管道在父进程和子进程之间进行通信。在父进程和子进程之间建立一个管道, 子进程向管道中写入数据,父进程从管道中读取数据。要实现这样的模型,在父进程中需要关闭写端,在子进程中需要关闭读端。
#include <iostream>
#include <unistd.h>
#include <string>
#include <string.h>
#include <cerrno>
#include <cassert>
#include <sys/types.h>
using namespace std;
int main()
{
// 任何一种任何一种进程间通信中,一定要先保证不同的进程之间看到同一份资源
int pipefd[2] = {0};
//1. 创建管道
int n = pipe(pipefd);
if(n < 0)
{
std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
return 1;
}
//2.创建子进程
pid_t id=fork();
assert(id != -1);
if(id==0)
{
//子进程
//3.关闭不需要的fd,让父进程进行读取,子进程进行写入
close(pipefd[0]);
//4.开始通信
string strmessage="hello,我是子进程";
char buffer[1024];
int count=1;
while(true)
{
snprintf(buffer,sizeof buffer,"%s,计数器:%d,我的PId:%d",strmessage.c_str(),count++,getpid());
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//3.关闭不需要的fd,让父进程进行读取,子进程进行写入
close(pipefd[1]);
//4.开始通信
char buffer[1024];
while(true)
{
int n=read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
cout<<"我是父进程,child give me message:"<<buffer<<endl;
}
}
close(pipefd[0]);
return 0;
}
特点
- 单向通信
- 管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的。
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。 (pipe打开管道,并不清楚管道的名字---匿名管道)。
- 在管道通信中,写入的次数,和读取的次数,不是严格匹配的读写次数的多少没有强相关。
- 具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信---自带同步机制。
#include <iostream>
#include <string>
#include <cerrno>
#include <cassert>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//1. 创建管道
int n = pipe(pipefd);
if(n < 0)
{
std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 读端, 0->嘴巴->读书
std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 写端, 1->笔->写东西的
//2. 创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)// 子进程
{
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[0]);
//4. 开始通信 -- 结合某种场景
// const std::string namestr = "hello, 我是子进程";
// int cnt = 1;
// char buffer[1024];
int cnt = 0;
while(true)
{
char x = 'X';
write(pipefd[1], &x, 1);
std::cout << "Cnt: " << cnt++<<std::endl;
sleep(1);
// break;
// snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid());
// write(pipefd[1], buffer, strlen(buffer));
}
close(pipefd[1]);
exit(0);
}
//父进程
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[1]);
//4. 开始通信 -- 结合某种场景
char buffer[1024];
int cnt = 0;
while(true)
{
// sleep(10);
// sleep(1);
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程, child give me message: " << buffer << std::endl;
}
else if(n == 0)
{
std::cout << "我是父进程, 读到了文件结尾" << std::endl;
break;
}
else
{
std::cout << "我是父进程, 读异常了" << std::endl;
break;
}
sleep(1);
if(cnt++ > 5) break;
}
close(pipefd[0]);
int status = 0;
waitpid(id, &status, 0);
std::cout << "sig: " << (status & 0x7F) << std::endl;
sleep(100);
return 0;
}
- 如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待。read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- 如果我们writer端将管道写满了,我们就不能写了。write调用阻塞,直到有进程读走数据。
- 如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾。
- 写端一直写,读端关闭,这是没有意义的。OS不会维护无意义、低效率或者浪费资源的事情。OS会杀死一直在写入的进程。OS会通过信号来终止进程(SIGPIPE--信号13)。
命名管道
命名管道的工作方式与普通的管道非常相似,但也有一些明显的区别。
- 在文件系统中命名管道是以设备特殊文件的形式存在的。
- 不同的进程可以通过命名管道共享数据。
创建FIFO
有许多种方法可以创建命名管道。其中,可以直接用shell来完成。例如,在当前目录下建立一一个名字为namedfifo的命名管道:
mkfifo namedfifo
可以看出namedfifo的属性中有一个p,表示这是个管道。
命名管道也可以从程序里创建,相关函数有:
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *filename,mode_t mode);
创建命名管道:
int main(int argc, char *argv[]) {
mkfifo("p2", 0644);
return 0;
}
FIFO 操作
对命名管道FIFO来说,IO操作与普通的管道IO操作基本上是一样的,二者之间存在着一个主要的区别。在FIFO中,必须使用一个open()函数来显式地建立连接到管道的通道。一般来说FIFO总是处于阻塞状态。也就是说,如果命名管道FIFO打开时设置了读权限,则读进程将一直“阻塞”,一直到其他进程打开该FIFO并且向管道中写入数据。这个阻塞动作反过来也是成立的,如果一个进程打开一个管道写入数据,当没有进程冲管道中读取数据的时候,写管道的操作也是阻塞的,直到已经写入的数据被读出后,才能进行写入操作。如果不希望在进行命名管道操作的时候发生阻塞,可以在open()调用中使用O_NONBLOCK标志,以关闭默认的阻塞动作。
用命名管道实现server&client通信
comm.hpp(client.cc和server.cc的命名管道)
#pragma once
#include <iostream>
#include <string>
#define NUM 1024
const std::string fifoname="./fifo";
uint32_t mode = 0666;
server.cc
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
int main()
{
// 1. 创建管道文件,只需要一次创建
umask(0); //这个设置并不影响系统的默认配置,只会影响当前进程
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 1;
}
std::cout << "create fifo file success" << std::endl;
// 2. 让服务端直接开启管道文件
int rfd = open(fifoname.c_str(), O_RDONLY);//只读方式打开
if(rfd < 0 )
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 2;
}
std::cout << "open fifo success, begin ipc" << std::endl;
// 3. 正常通信
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
std::cout << "client# " << buffer << std::endl;
//printf("%c", buffer[0]);
//fflush(stdout);
}
else if(n == 0)
{
std::cout << "client quit, me too" << std::endl;
break;
}
else
{
std::cout << errno << " : " << strerror(errno) << std::endl;
break;
}
}
// 关闭不要的fd
close(rfd);
unlink(fifoname.c_str());
return 0;
}
client.cc
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// #include <ncurses.h>
#include "comm.hpp"
int main()
{
//1. 不需创建管道文件,我只需要打开对应的文件即可!
int wfd = open(fifoname.c_str(), O_WRONLY);//只写方式打开
if(wfd < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
// 可以进行常规通信了
char buffer[NUM];
while(true)
{
std::cout<<"请输入你的消息#:";
char *msg = fgets(buffer,sizeof(buffer),stdin);
assert(msg);
(void)msg;
buffer[strlen(buffer)-1]=0;
if(strcasecmp(buffer,"quit") == 0) break;
ssize_t n = write(wfd,buffer,strlen(buffer));
assert(n > 0);
(void)n;
}
close(wfd);
return 0;
}
共享内存
共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它是在多个进程之间对内存段进行映射的方式实现内存共享的。这是IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换;与此相反,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅是地址不同而已,因此不需要进行复制,可以直接使用此段空间。
创建共享内存函数shmget()
函数shmget()用于创建一个新的共享内存段, 或者访问一个现有的共享内存段。函数shmget()的原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_ t size, int shmflg);key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmget()的第一个参数是关键字的值。然后,这个值将与内核中现有的其他共享内存段的关键字值相比较。在比较之后,打开和访问操作都将依赖于shmflg参数的内容。
- IPC_CREAT:如果在内核中不存在该内存段,则创建它。
- IPC_EXCL:当与IPC CREAT 一起使用时,如果该内存段早已存在,则此次调用将失败。
如果只使用IPC_CREAT, shmget()或者 将返回新创建的内存段的段标识符,或者返回早已存在于内核中的具有相同关键字值的内存段的标识符。如果同时使用IPC_CREAT和IPC_EXCL,则可能会有两种结果:如果该内存段不存在,则将创建一个新的内存段;如果内存段早已存在,则此次调用失败,并将返回-1。IPC_EXCL本身是没有什么用处的,但在与IPC_CREAT组合使用时,它可用于防止一个现有的内存段为了访问而打开着。一旦进程获得了给定内存段的合法IPC标识符,它的下一步操作就是连接该内存段,或者把该内存段映射到自己的寻址空间中。
单独使用 IPC_CREAT :创建一个共享内存,如果共享内存不存在 ,就创建它;如果存在,就获取已经存在的共享内存并且返回
IPC_EXCT不能单独使用,一般都要配合IPC_CREAT
IPC_CREAT | IPC_EXCT:创建一个共享内存,如果共享内存不存在,就创建它,如果存在,出错并返回(如果共享内存创建成功,则这个共享内存一定是最新的)
获得共享内存地址函数shmat()
函数shmat()用来获取共享内存的地址,获取共享内存成功后,可以像使用通用内存一样对其进行读写操作。函数的原型如下:
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1
如果shmaddr参数值等于0,则内核将试着查找一个未映射的区域。用户可以指定个地址,但通常该地址只用于访问所拥有的硬件,或者解决与其他应用程序的冲突。SHM_RND标志可以与标志参数进行OR操作,结果再置为标志参数,这样可以让传送的地址页对齐。
此外,如果把SHM_RDONLY标志与标志参数进行OR操作,结果再置为标志参数,这样映射的共享内存段只能标记为只读方式。
当申请成功时,对共享内存的操作与一般内存一样,可以直接进行写入和读出,以及偏移的操作。
删除共享内存函数shmdt()
函数shmdt()用于删除一段共享内存。 函数的原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt (const void *shmaddr) ;shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
当某进程不再需要一个共享内存段时,它必须调用这个函数来断开与该内存段的连接。这与从内核删除内存段是两回事。在成功完成了断开连接操作以后,相关的shmid ds结构的shm nattch成员的值将减去1。如果这个值减到0,则内核将真正删除该内存段。
共享内存控制函数shmctl()
共享内存的控制函数shmctl的使用类似iocl()的方式对共享内存进行操作:向共享内存的句柄发送命令来完成某种功能。函数shmcl()的原型如下,其中shmid是其享内存的句柄,emd是向共享内存发送的命令,最后一个参数 buf则是向共享内存发送命令的参数。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf) ;shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
- IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值。
- IPC_SET:获取内存段的shmid_ds结构,并把它存储在buf参数所指定的地址中。IPC_ SET设置内存段shmid_ds 结构的ipc_perm 成员的值,此命令是从buf参数中获得该值的。
- IPC_ RMID:标记某内存段,以备删除。该命令并不真正地把内存段从内存中删除。相反,它只是标记上该内存段,以备将来删除。只有当前连接到该内存段的最后一个进程正确地断开了与它的连接,实际的删除操作才会发生。当然,如果当前没有进程与该内存段相连接,则删除将立刻发生。为了正确地断开与其共享内存段的连接,进程需要调用shmdt()函数。
小实验:
comm.hpp(对方法的封装)
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
// IPC_CREAT and IPC_EXCT
// 单独使用 IPC_CREAT :创建一个共享内存,如果共享内存不存在 ,就创建它;如果存在,就获取已经存在的共享内存并且返回
// IPC_EXCT不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT |IPC_EXCT:创建一个共享内存,如果共享内存不存在,就创建它,如果存在,出错并返回(如果共享内存创建成功,则这个共享内存一定是最新的)
#define PATHNAME "."
#define PROJID 0x6666
const int gsize = 4096;
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJID);
if (k == -1)
{
cerr << "error:" << errno << strerror(errno) << endl;
exit(1);
}
return k;
}
// 十六进制转换
string toHex(int x)
{
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
static int createShmHelper(key_t k, int size, int flag)
{
int shmid = shmget(k, size, flag);
if (shmid == -1)
{
cerr << "error:" << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int createShm(key_t k, int size)
{
umask(0);
return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(key_t k, int size)
{
umask(0);
return createShmHelper(k, size, IPC_CREAT);
}
char *attachShm(int shmid)
{
char *start = (char *)shmat(shmid, nullptr, 0);
return start;
}
void detachShm(char *start)
{
int n = shmdt(start);
assert(n != -1);
(void)n;
}
void delShm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
}
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int t)
:type(t)
{
key_t k = getKey();
if(type == SERVER)
shmid = createShm(k,gsize);
else
shmid = getShm(k,gsize);
start = attachShm(shmid);
}
char* getStart(){return start;}
~Init()
{
detachShm(start);
if(type == SERVER)
delShm(shmid);
}
private:
char*start;
int type;//server or client
int shmid;
};
#endif
server.cc
#include "comm.hpp"
#include <unistd.h>
int main()
{
Init init(SERVER);
char* start = init.getStart();
int n = 0;
while(n <= 26)
{
cout<<"client -> server#"<<start<<endl;
sleep(1);
n++;
}
// //1.创建key
// key_t k = getKey();
// cout<<"server key:"<<toHex(k)<<endl;
// //2.创建共享内存
// int shmid = createShm(k,gsize);
// cout<<"server shmid:"<<shmid<<endl;
// //3.将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(5);
// //4.将自己和共享内存去关联
// detachShm(start);
// //删除共享内存
// delShm(shmid);
return 0;
}
client.cc
#include "comm.hpp"
#include <unistd.h>
#include <string>
#include <vector>
int main()
{
Init init(CLIENT);
char* start = init.getStart();
char c = 'A';
while(c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = '\0';
sleep(1);
}
// //1.创建key
// key_t k = getKey();
// cout<<"client key:"<<toHex(k)<<endl;
// //2.创建共享内存
// int shmid = createShm(k,gsize);
// cout<<"client shmid:"<<shmid<<endl;
// //3.将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(10);
// //4.将自己和共享内存去关联
// detachShm(start);
return 0;
}
信号量
信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。它们常常被用作做一个锁机制,在某个进程正在对特定资源进行操作时,信号量可以防止另一个进程去访问它。生产者和消费者的模型是信号量的典型使用。
信号量数据结构
信号量数据结构是信号量程序设计中经常使用的数据结构。
union semun { /*信号量操作的联合结构*/
int val; /*整型变量*/
struct semid ds *buf; /*semid_ ds结构指针*/
unsigned short *array; /*数组类型*/
struct seminfo *_buf; /*信号量内部结构*/
};
新建信号量函数semget()
semget()函数用于创建一个新的信号量集合, 或者访问现有的集合。 其原型如下,其中第1个参数key是ftok 生成的键值,第2个参数nsems参数可以指定在新的集合中应该创建的信号量的数目,第3个参数semflsg是打开信号量的方式。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_ t key, int nsems, int semflg);
semflsg是打开信号量的方式。
IPC_ CREAT:如果内核中不存在这样的信号量集合,则把它创建出来。
IPC_ EXCL: 当与IPC_CREAT一起使用时,如果信号量集合早已存在,则操作将失败。如果单独使用IPC_ CREAT, semget()或者返回新创建的信号量集合的信号量集合标识符;或者返回早已存在的具有同一个关键字值的集合的标识符。如果同时使用IPC_ EXCL和IPC_ CREAT,那么将有两种可能的结果:如果集合不存在,则创建一个新的集合;如果集合早已存在,则调用失败,并返回-1。 IPC_EXCL本身是没有什么用处的,但当与IPC_CREAT组合使用时,它可以用于防止为了访问而打开现有的信号量集合。
typedef int sem_t;
union semun { /*信号量操作的联合结构*/
int val; /*整型变量*/
struct semid ds * buf; /*semid_ ds结构指针*/
unsigned short* array; /*数组类型*/
}arg;
sem_t CreateSem(key_t key, int value)//建立信号量
{
union semun sem;/*信号量结构变量*/
sem t semid;/*信号量ID*/
sem.val = value;/*设置初始值*/
semid = semget(key, 0, IPC_ CREAT | 0666);/*获得信号量的ID*/
if (-1 == semid)/*获得信号量ID失败*/
{
printf("create semaphore error\n"); /*打印信息*/
return -1;/*返回错误*/
}
semctl(semid, 0,SETVAL, sem);/*发送命令,建立value个初始值的信号量*/
return semid;/*返回建立的信号量*/
}
CealeSem()函数按照用户的键值生成个信号量,把信号量的初始值设为用户输入的value。
信号量操作函数semop()
信号量的P、V操作是通过向已经建立好的信号量(使用semget()函数),发送命令来完成的。向信号信号量发送命令的函数是semop(),这个函数的原型如下:
#include<sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops,unsigned nsops) ;
semop()函数第2个参数(sops)是一个指针,指向将要在信号量集合上执行操作的一个数组,第3个参数(nsops)则是该数组中操作的个数。sops参数指向的是类型为sembuf结构的一个数组。sembuf结构是在linux/sem.h中定义的,如下所示。
struct sembuf {
ushort sem_num; /*信号量的编号*/
short sem_op; /*信号量的操作*/
short sem_ flg; /*信号量的操作标志*/}
- sem_num:用户要处理的信号量的编号。
- sem_op:将要执行的操作(正、负,或者零)。
- sem_flg: 信号量操作的标志。如果sem_op为负,则从信号量中减掉一个值。如果sem_op为正,则从信号量中加上值。如果sem_op为0,则将进程设置为睡眠状态,直到信号量的值为0为止。
例如“stuet sembuf sem={0, +1, NOWAIT}:" 表示对信号量0,进行加1的操作。用函数semop()可以构建基本的P、V操作,代码如下所示。Sem_P构建{0, +1, NOWAIT}的sembuf结构来进行增加1个信号量值的操作: Sem_V构建{0,-1, NOWAIT}的sembuf结构来进行减少1个信号量的操作,所对应的信号量由函数传入(semid)。
int Sem P(sem t semid) /*增加信号量*/
{
struct sembuf sops={0,+1, IPC NOWAIT} ; /*建立信号量结构值*/
return (semop (semid, &sops,1)) ; /*发送命令*/
}
int Sem V(sem t semid) /*减小信号量值*/
{
struct sembuf sops={0,-1,IPC NOWAIT} ; /*建立信号量结构值*/
return (semop (semid, &sops,1)); /*发送信号量操作方法*/
}
控制信号量参数semctl()函数
与文件操作的ioctl()函数类似,信号量的其他操作是通过函数semctl()来完成的。函數semctl()的原型如下:
#include <sys/types
#include <sys/types.h>
#include <sys/sem. h
int semctl(int semid, int semnum, int cmd, .. );
函数semctl()用于在信号量集合上执行控制操作。这个调用类似函数msgctl(),msgctl()函数是用于消息队列上的操作。semctl()函数的第1个参数是关键字的值(在我们的例子中它是调用semget)函数所返回的值)。第2个参数(semun)是将要执行操作的信号量的编号,它是信号量集合的一个索引值, 对于集合中的第1个信号量(有可能只有这一个信号量)来说,它的索引值将是一个为0的值。cmd 参数代表将要在集合上执行的命令 :
- IPC_STAT: 获取某个集合的semid_ds结构,并把它存储在semun联合体的buf参数所指定的地址中。
- IPC_SET: 设置某个集合的semid_ds 结构的ipc_perm成员的值。该命令所取的值是从semun联合体的buf参数中取到的。
- IPC_ _RMID: 从内核删除该集合。
- GETALL:用于获取集合中所有信号量的值。整数值存放在无符号短整数的一个数组中,该数组由联合体的array成员所指定。
- GETNCNT:返回当前正在等待资源的进程的数目。
- GETPID: 返回最后一 次执行semop调用的进程的PID。
- GETVAL: 返回集合中某个信号量的值。
- GETZCNT: 返回正在等待资源利用率达到百分之百的进程的数目。
- SETALL:把集合中所有信号量的值,设置为联合体的array成员所包含的对应值。
- SETVAL:把集合中单个信号量的值设置为联合体的val成员的值。
void SetvalueSem(sem_t semid, int val)
{
union semun sem;
sem.val = value;
semctl(semid, 0, SETVAL, sem);
}
void GetvalueSem(sem_t semid)
{
union semun sem;
return semctl(semid, 0, GETVAL, sem);
}
SetvalueSem()函数设置信号量的值,它是通过SETVAL命令实现的,所设置的值通过联合变量sem的val域实现。GetvalueSem()函数用于获取信号量的值,semctl()函数的命令GETVAL会使其返回给定信号量的当前值。当然,销毁信号量同样可以使用semctl()函数实现。
void DeatroySem(sem_t semid)
{
union semun sem;
sem.val = 0;
semctl(semid, 0, IPC_RMID, sem);
}