IPC Inter-Process Communication,进程间通信

IPC是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。



IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。

进程间通信(IPC,Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法。进程是计算机系统分配资源的最小单位(严格说来是线程)。每个进程都有自己的一部分独立的系统资源,彼此是隔离的。为了能使不同的进程互相访问资源并进行协调工作,才有了进程间通信。举一个典型的例子,使用进程间通信的两个应用可以被分类为客户端和服务器,客户端进程请求数据,服务端回复客户端的数据请求。有一些应用本身既是服务器又是客户端,这在分布式计算中,时常可以见到。这些进程可以运行在同一计算机上或网络连接的不同计算机上。
进程间通信技术包括消息传递、同步、共享内存和远程过程调用。IPC是一种标准的Unix通信机制。



进程间通信是指在不同进程之间传播或交换信息,在Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间,进程之间不能相互访问。必须通过内核才能进行数据交换



常见的通信方式有以下几种:



管道pipe
有名管道FIFO
消息队列MessageQueue
共享存储
信号量Semaphore
信号Signal
套接字Socket
接下来我们将详细介绍共享存储



共享存储
内存映射I/O
在讲解内存映射之前,我们先简单了解一些虚拟内存的概念。



虚拟内存为每个进程提供了一个大的、一致的和私有的地址空间,它提供了3个能力:



将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据。
它为每个进程提供了一致的地址空间,从未简化了内存管理。
它保护了每个进程的地址空间不被其他进程破坏。
VM系统将虚拟内存分割为虚拟页,物理内存也被分隔为物理页。
使用寄存器中的内存管理单元[MMU(Memory Management Unit)],利用存放在主存中的页表来动态翻译虚拟地址,就可以使用虚拟地址去访问相应的物理地址



内存映射
内存映射,通过将虚拟内存区域与磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容。
磁盘上的对象可以是两类,Linux文件系统中的普通文件,或是匿名文件(由内核创建)。



共享内存映射
将虚拟地址指向同一个物理地址。如图所示



mmap函数
Linux进程可以使用mmap函数来创建新的虚拟内存区域,并将对象映射到这些区域中。



void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
//成功:返回创建的映射区首地址;失败:MAP_FAILED宏
1
2
参数



addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
length: 欲创建映射区的大小
prot: 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
fd: 用来建立映射区的文件描述符
offset: 映射文件的偏移(4k的整数倍)
mmap建立的映射区在使用结束后也应调用类似free的函数来释放。



int munmap(void *addr, size_t length);
//成功:返回0; 失败:返回-1
1
2
mmap进程间通信
父子等有血缘关系的进程之间可以通过 mmap建立的映射区来完成数据通信。但是相应的要在创建映射区的时候指定对应的标志位参参数flags。
MAP_PRIVATE(私有映射)父子进程各自独占映射区
MAP_SHARED(共享映射)父子进程共享映射区
也可以通过匿名映射,不需要以来一个文件就能够实现。同样也需要依赖于标志位的设定(只适用于有血缘关系的进程之间通信)
使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
没有血缘关系的进程之间通信时需要,用open打开同一个文件 得到fd,再调mmap指定fd,将虚拟内存地映射到同一个物理内存地址上。
代码样例 父子进程间通信



#include
#include
#include
#include
#include <sys/mman.h>
#include <sys/wait.h>



int var = 100;



int main(void)
{
int *p;
pid_t pid;



int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放条件.
//将参数fd指定的文件大小改为参数length指定的大小
ftruncate(fd, 4);

//MAP_SHARED
//父子进程各自独占映射区
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//MAP_PRIVATE
//父子进程共享映射区
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件

pid = fork(); //创建子进程
if(pid == 0){
*p = 2000;
//非共享变量
//在子进程中改为1000,父进程中仍为100
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);

int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0; }


文件进程间通信
使用文件也可以完成进程间通信,父进程使用fork创建子进程后,父子进程共享文件描述符,也就是说,共享打开的文件。



/*
*父子进程共享打开的文件描述符——使用文件完成进程间通信.
*/
#include
#include
#include
#include
#include
#include <sys/wait.h>



int main(void)
{
int fd1, fd2; pid_t pid;
char buf[1024];
char *str = “———test for shared fd in parent child process—–\n”;



    pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
//子进程进入
} else if (pid == 0) {
fd1 = open("test.txt", O_RDWR);
if (fd1 < 0) {
perror("open error");
exit(1);
}
write(fd1, str, strlen(str));
printf("child wrote over...\n");

//父进程进入
} else {
fd2 = open("test.txt", O_RDWR);
if (fd2 < 0) {
perror("open error");
exit(1);
}
sleep(1); //保证子进程写入数据

int len = read(fd2, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);

wait(NULL);
}

return 0; }


看到这里你可能会有一些疑惑,文件进程间通信和有名管道FIFO有什么区别呢?



我们知道管道只有两端,读端和写端,有名管道可以控制读写两端,保证他们的同步性。
但是对于文件进程间进程通信而言,我们必须自己定义一些同步机制来控制读数据和写数据,否则可能会造成一些错误。



总结
共享存储是非常重要的一种进程间通信方式,理解起来也相对来说比较困难有点,但是在了解了一些基本概念之后再去学习就会容易得多



在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication),它是多个进程之间相互沟通的一种方法。在linux下有多种进程间通信的方法:半双工管道、命名管道、消息队列、信号、信号量、共享内存、内存映射文件,套接字等等。使用这些机制可以为linux下的网络服务器开发提供灵活而又坚固的框架。在这篇博客中我实现了其中的几种机制,详细如下:



1、无名管道:



  



  管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。



  管道的特点:



  (1)管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;



  (2)只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。



  (3)单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。



  (4)数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。



  管道的实现机制:



  管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。



  管道只能在本地计算机中使用,而不可用于网络间的通信。



复制代码
#include
#include <sys/types.h>
#include
#include
#include
#include



int main()
{
int pipe_fd[2];
pid_t pid;
char r_buf[10];
char w_buf[4];
int r_num;



memset(r_buf,0,sizeof(r_buf));
memset(w_buf,0,sizeof(w_buf));
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

if((pid=fork())==0)
{
printf("\n");
close(pipe_fd[1]);
sleep(3);//确保父进程关闭写端
r_num=read(pipe_fd[0],r_buf,10);
printf( "read num is %d the data read from the pipe is %d\n",r_num,atoi(r_buf));

close(pipe_fd[0]);
exit(1);
}
else if(pid>0)
{
close(pipe_fd[0]);//close read
strcpy(w_buf,"111");
if(write(pipe_fd[1],w_buf,4)!=-1)
printf("parent write over\n");
printf("parent close fd[1] over\n");
close(pipe_fd[1]);//write
sleep(10);
}
return 0; } 复制代码


2、有名管道:



  命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了无名管道的弊端,他可以允许没有亲缘关系的进程间通信。



  无名管道和命名管道的区别:



  对于命名管道FIFO来说,IO操作和普通管道IO操作基本一样,但是两者有一个主要的区别,在命名管道中,管道可以是事先已经创建好的,比如我们在命令行下执行mkfifo myfifo就是创建一个命名通道,我们必须用open函数来显示地建立连接到管道的通道,而在管道中,管道已经在主进程里创建好了,然后在fork时直接复制相关数据或者是用exec创建的新进程时把管道的文件描述符当参数传递进去。



  一般来说FIFO和PIPE一样总是处于阻塞状态。也就是说如果命名管道FIFO打开时设置了读权限,则读进程将一直阻塞,一直到其他进程打开该FIFO并向管道写入数据。这个阻塞动作反过来也是成立的。如果不希望命名管道操作的时候发生阻塞,可以在open的时候使用O_NONBLOCK标志,以关闭默认的阻塞操作。



复制代码
//writing
#include
#include
#include
#include
#include <sys/types.h>
#include <sys/stat.h>
#include
#include



#define N 80



int main() {
int out_file;
int nbyte;
char buf[N];
if((mkfifo(“myfifo”,0666))<0) //创建有名管道
{
if(errno==EEXIST)
{
printf(“The fifo is exist.\n”);
}
else{
perror(“creat myfifo failed!\n”);
exit(-1);
}
}else{
printf(“created by this process.\n”);
}
out_file = open(“myfifo”,O_WRONLY);
if (out_file < 0) {
printf(“Error opening fifo.”);
exit(1);
}
printf(“please input something:\n”);
while((nbyte = read(0,buf,N))){
write(out_file,buf,nbyte);
printf(“please input something:\n”);
}
close(out_file);
return 0;
}
复制代码



复制代码
//reading
#include
#include
#include
#include <sys/types.h>
#include <sys/stat.h>
#include
#include
#define N 80



int main(void) {
int in_file;
int count = 1;
char buf[N];
if((mkfifo(“myfifo”,0666))<0)//创建有名管道
{
if(errno==EEXIST)//管道已经存在
{
printf(“The fifo is exist.\n”);
}
else{
printf(“creat myfifo failed!\n”);
exit(-1);
}
}
else
{
printf(“created by this process.\n”);
}
in_file = open(“myfifo”,O_RDONLY);
if (in_file < 0) {
printf(“Error in opening.\n”);
exit(1);
}



while ((count = read(in_file,buf,N)) > 0)
{
printf("received from fifo: %s\n", buf);
memset(buf,0,N);
}
close(in_file);
return 0; } 复制代码


3、C/S:



复制代码
//client.c
#include
#include
#include <sys/msg.h>



// 用于创建一个唯一的key
#define MSG_FILE “/etc/passwd”



// 消息结构
struct msg_form {
long mtype;
char mtext[256];
};



int main()
{
int msqid;
key_t key;
struct msg_form msg;



// 获取key值
if ((key = ftok(MSG_FILE, 'z')) < 0)
{
perror("ftok error");
exit(1);
}

// 打印key值
printf("Message Queue - Client key is: %d.\n", key);

// 打开消息队列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}

// 打印消息队列ID及进程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());

// 添加消息,类型为888
msg.mtype = 888;
sprintf(msg.mtext, "hello, I'm client %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);

// 读取类型为777的消息
msgrcv(msqid, &msg, 256, 999, 0);
printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
return 0; } 复制代码


复制代码
//server.c
#include
#include
#include <sys/msg.h>



// 用于创建一个唯一的key
#define MSG_FILE “/etc/passwd”



// 消息结构
struct msg_form {
long mtype;
char mtext[256];
};



int main()
{
int msqid;
key_t key;
struct msg_form msg;



// 获取key值
if((key = ftok(MSG_FILE,'z')) < 0)
{
perror("ftok error");
exit(1);
}

// 打印key值
printf("Message Queue - Server key is: %d.\n", key);

// 创建消息队列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}

// 打印消息队列ID及进程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());

// 循环读取消息
for(;;)
{
msgrcv(msqid, &msg, 256, 888, 0);// 返回类型为888的第一个消息
printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
printf("Server: receive msg.mtype is: %d.\n", msg.mtype);

msg.mtype = 999; // 客户端接收的消息类型
sprintf(msg.mtext, "hello, I'm server %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
}
return 0; } 复制代码


4、读者/写者:



复制代码
//write
#include
#include // exit
#include // O_WRONLY
#include<sys/stat.h>
#include // time



int main()
{
int fd;
int n, i;
char buf[1024];
time_t tp;



printf("I am %d process.\n", getpid()); // 说明进程ID

if((fd = open("fifo1", O_WRONLY)) < 0) // 以写打开一个FIFO
{
perror("Open FIFO Failed");
exit(1);
}

for(i=0; i<10; ++i)
{
time(&tp); // 取系统当前时间
n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
printf("Send message: %s", buf); // 打印
if(write(fd, buf, n+1) < 0) // 写入到FIFO中
{
perror("Write FIFO Failed");
close(fd);
exit(1);
}
sleep(1); // 休眠1秒
}

close(fd); // 关闭FIFO文件
return 0; } 复制代码


复制代码
//read
#include
#include
#include
#include
#include<sys/stat.h>



int main()
{
int fd;
int len;
char buf[1024];



if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 创建FIFO管道
perror("Create FIFO Failed");

if((fd = open("fifo1", O_RDONLY)) < 0) // 以读打开FIFO
{
perror("Open FIFO Failed");
exit(1);
}

while((len = read(fd, buf, 1024)) > 0) // 读取FIFO管道
printf("Read message: %s", buf);

close(fd); // 关闭FIFO文件
return 0; } 复制代码


5、信号量机制:



  信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。



  信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号)
  P(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行



  V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1
  简单理解就是P相当于申请资源,V相当于释放资源



  操作系统课程里面大量提到过信号量机制,故在此不赘述。



复制代码
#include
#include
#include<sys/sem.h>



// 联合体,用于semctl初始化
union semun
{
int val; /for SETVAL/
struct semid_ds *buf;
unsigned short *array;
};



// 初始化信号量
int init_sem(int sem_id, int value)
{
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1)
{
perror(“Init Semaphore Error”);
return -1;
}
return 0;
}



// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = -1; /P操作/
sbuf.sem_flg = SEM_UNDO;



if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0; }


// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = 1; /V操作/
sbuf.sem_flg = SEM_UNDO;



if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0; }


// 删除信号量集
int del_sem(int sem_id)
{
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
{
perror(“Delete Semaphore Error”);
return -1;
}
return 0;
}



int main()
{
int sem_id; // 信号量集ID
key_t key;
pid_t pid;



// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}

// 创建信号量集,其中只有一个信号量
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
{
perror("semget error");
exit(1);
}

// 初始化:初值设为0资源被占用
init_sem(sem_id, 0);

if((pid = fork()) == -1)
perror("Fork Error");
else if(pid == 0) /*子进程*/
{
sleep(2);
printf("Process child: pid=%d\n", getpid());
sem_v(sem_id); /*释放资源*/
}
else /*父进程*/
{
sem_p(sem_id); /*等待资源*/
printf("Process father: pid=%d\n", getpid());
sem_v(sem_id); /*释放资源*/
del_sem(sem_id); /*删除信号量集*/
}
return 0; } 复制代码


6、共享内存:



  共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。



  共享内存是IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。



  注意:共享内存本身并没有同步机制,需要程序员自己控制。



复制代码
////share_mem_client
#include
#include
#include<sys/shm.h> // shared memory
#include<sys/sem.h> // semaphore
#include<sys/msg.h> // message queue
#include // memcpy



// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};



// 联合体,用于semctl初始化
union semun
{
int val; /for SETVAL/
struct semid_ds *buf;
unsigned short *array;
};



// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = -1; /P操作/
sbuf.sem_flg = SEM_UNDO;



if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0; }


// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = 1; /V操作/
sbuf.sem_flg = SEM_UNDO;



if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0; }


int main()
{
key_t key;
int shmid, semid, msqid;
char shm;
struct msg_form msg;
int flag = 1; /
while循环条件*/



// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}

// 获取共享内存
if((shmid = shmget(key, 1024, 0)) == -1)
{
perror("shmget error");
exit(1);
}

// 连接共享内存
shm = (char*)shmat(shmid, 0, 0);
if((int)shm == -1)
{
perror("Attach Shared Memory Error");
exit(1);
}

// 创建消息队列
if ((msqid = msgget(key, 0)) == -1)
{
perror("msgget error");
exit(1);
}

// 获取信号量
if((semid = semget(key, 0, 0)) == -1)
{
perror("semget error");
exit(1);
}

// 写数据
printf("***************************************\n");
printf("* IPC *\n");
printf("* Input r to send data to server. *\n");
printf("* Input q to quit. *\n");
printf("***************************************\n");

while(flag)
{
char c;
printf("Please input command: ");
scanf("%c", &c);
switch(c)
{
case 'r':
printf("Data to send: ");
sem_p(semid); /*访问资源*/
scanf("%s", shm);
sem_v(semid); /*释放资源*/
/*清空标准输入缓冲区*/
while((c=getchar())!='\n' && c!=EOF);
msg.mtype = 888;
msg.mtext = 'r'; /*发送消息通知服务器读数据*/
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
break;
case 'q':
msg.mtype = 888;
msg.mtext = 'q';
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
flag = 0;
break;
default:
printf("Wrong input!\n");
/*清空标准输入缓冲区*/
while((c=getchar())!='\n' && c!=EOF);
}
}

// 断开连接
shmdt(shm);

return 0; } 复制代码


复制代码
//share_mem_server
#include
#include
#include<sys/shm.h> // shared memory
#include<sys/sem.h> // semaphore
#include<sys/msg.h> // message queue
#include // memcpy



// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};



// 联合体,用于semctl初始化
union semun
{
int val; /for SETVAL/
struct semid_ds *buf;
unsigned short *array;
};



// 初始化信号量
int init_sem(int sem_id, int value)
{
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1)
{
perror(“Init Semaphore Error”);
return -1;
}
return 0;
}



// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = -1; /P操作/
sbuf.sem_flg = SEM_UNDO;



if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0; }


// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = 1; /V操作/
sbuf.sem_flg = SEM_UNDO;



if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0; }


// 删除信号量集
int del_sem(int sem_id)
{
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
{
perror(“Delete Semaphore Error”);
return -1;
}
return 0;
}



// 创建一个信号量集
int creat_sem(key_t key)
{
int sem_id;
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
{
perror(“semget error”);
exit(-1);
}
init_sem(sem_id, 1); /初值设为1资源未占用/
return sem_id;
}



int main()
{
key_t key;
int shmid, semid, msqid;
char shm;
char data[] = “this is server”;
struct shmid_ds buf1; /
用于删除共享内存/
struct msqid_ds buf2; /
用于删除消息队列/
struct msg_form msg; /
消息队列用于通知对方更新了共享内存*/



// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}

// 创建共享内存
if((shmid = shmget(key, 1024, IPC_CREAT|0666)) == -1)
{
perror("Create Shared Memory Error");
exit(1);
}

// 连接共享内存
shm = (char*)shmat(shmid, 0, 0);
if((int)shm == -1)
{
perror("Attach Shared Memory Error");
exit(1);
}


// 创建消息队列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}

// 创建信号量
semid = creat_sem(key);

// 读数据
while(1)
{
msgrcv(msqid, &msg, 1, 888, 0); /*读取类型为888的消息*/
if(msg.mtext == 'q') /*quit - 跳出循环*/
break;
if(msg.mtext == 'r') /*read - 读共享内存*/
{
sem_p(semid);
printf("%s\n",shm);
sem_v(semid);
}
}

// 断开连接
shmdt(shm);

/*删除共享内存、消息队列、信号量*/
shmctl(shmid, IPC_RMID, &buf1);
msgctl(msqid, IPC_RMID, &buf2);
del_sem(semid);
return 0; } 复制代码


7、网际套接字:



  套接字是计算机网络课程、java课程上见过面的老朋友了。套接字机制不但可以单机的不同进程通信,而且使得跨网机器间进程可以通信。



  套接字的创建和使用与管道是有区别的,套接字明确地将客户端与服务器区分开来,可以实现多个客户端连到同一服务器。



  (1)服务器套接字连接过程描述:



  首先,服务器应用程序用socket创建一个套接字,它是系统分配服务器进程的类似文件描述符的资源。 接着,服务器调用bind给套接字命名。这个名字是一个标示符,它允许linux将进入的针对特定端口的连接转到正确的服务器进程。 然后,系统调用listen函数开始接听,等待客户端连接。listen创建一个队列并将其用于存放来自客户端的进入连接。 当客户端调用connect请求连接时,服务器调用accept接受客户端连接,accept此时会创建一个新套接字,用于与这个客户端进行通信。



  (2)客户端套接字连接过程描述:



  客户端首先调用socket创建一个未命名套接字,让后将服务器的命名套接字作为地址来调用connect与服务器建立连接。



  只要双方连接建立成功,我们就可以像操作底层文件一样来操作socket套接字实现通信。



复制代码
   //server.c
#include
#include
#include
#include <netinet/in.h>
#include <arpa/inet.h>



int main(void)
{
//create socket
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd==-1)
{
perror("socket\n");
exit(-1);
}
printf("socket fd=%d\n",fd);

//build connection address
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");

int r;
r = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
if(r==-1)
{
perror("bind");
close(fd);
exit(-1);
}
printf("bind address successful!\n");
//accept or send message
char buf[255];
struct sockaddr_in from;
socklen_t len;
len = sizeof(from);
while(1)
{
r = recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&from,&len);
if(r>0)
{
buf[r]=0;
printf("The message from %s is:%s\n",inet_ntoa(from.sin_addr),buf);
}
else
{
break;
}
}
//close socket
close(fd);
return 0;
} 复制代码


复制代码
//client.c
#include
#include
#include
#include
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>



int main(void)
{
//create socket
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd==-1)
{
perror("socket");
exit(-1);
}
printf("create socket OK!\n");
//create an send address
struct sockaddr_in addr={};
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr=inet_addr("127.0.0.1");
//send the message to the specify address
int r;
char buf[255];
while(1)
{
r = read(0,buf,sizeof(buf)-1);
if(r<=0)
break;
sendto(fd,buf,r,0,(struct sockaddr*)&addr,sizeof(addr));
}
//close socket
close(fd);
return 0;
}


消息队列


    消息队列能够克服早期UNIX通信机制的一些缺点。作为早期UNIX通信机制之一的信号能够传送的信息量有限,但是信号这种通信方式更像“即时”的通信方式,它要求接收信号的进程在某个时间范围内对信号作出反应。消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式及特定的优先级。对消息队列有写权限的进程可以按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内核持续的。



    目前有两种消息队列———POSIX消息队列和系统V消息队列。系统V消息队列目前被大量使用,考虑到程序的可移植性,新开发的系统应尽量使用POSIX消息队列。



对消息队列的操作有下面三种类型。



(1)打开或创建消息队列



(2)读写操作



struct msgbuf
{
long mtype;
char mtext[1];
}
 (3)获得或设置消息队列属性



信号灯



    信号灯与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存的标志,进程可以个根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于控制外,还可用于进程同步。信号灯有以下两种类型。



    (1)二值信号灯:最简单的信号灯形式,信号灯的值只能取0、1.类似于互斥锁。



    (2)计算信号灯:信号灯的值可以取任意非负值。



对信号灯的操作:



(1)打开创建信号灯;



(2)信号灯值操作;



(3)获得或设置信号灯属性;



共享内存



共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间,进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一内存区域,必然需要某种同步机制,互斥锁和信号量都可以



1.管道(pipe)及有名管道(named pipe):



管道可用于具有亲缘关系进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。



2.信号(signal):



信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致得。



3.消息队列(message queue):



消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。



消息缓冲通信技术是由Hansen首先提出的,其基本思想是:根据”生产者-消费者”原理,利用内存中公用消息缓冲区实现进程之间的信息交换.



内存中开辟了若干消息缓冲区,用以存放消息.每当一个进程向另一个进程发送消息时,便申请一个消息缓冲区,并把已准备好的消息送到缓冲区,然后把该消息缓冲区插入到接收进程的消息队列中,最后通知接收进程.接收进程收到发送里程发来的通知后,从本进程的消息队列中摘下一消息缓冲区,取出所需的信息,然后把消息缓冲区不定期给系统.系统负责管理公用消息缓冲区以及消息的传递.



一个进程可以给若干个进程发送消息,反之,一个进程可以接收不同进程发来的消息.显然,进程中关于消息队列的操作是临界区.当发送进程正往接收进程的消息队列中添加一条消息时,接收进程不能同时从该消息队列中到出消息:反之也一样.



消息缓冲区通信机制包含以下列内容:



(1) 消息缓冲区,这是一个由以下几项组成的数据结构:

1、 消息长度

2、 消息正文

3、 发送者

4、 消息队列指针



(2)消息队列首指针m-q,一般保存在PCB中。

(1) 互斥信号量m-mutex,初值为1,用于互斥访问消息队列,在PCB中设置。

(2) 同步信号量m-syn,初值为0,用于消息计数,在PCB中设置。

(3) 发送消息原语send

(4) 接收消息原语receive(a)



4.共享内存(shared memory):



可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。



这种通信模式需要解决两个问题:第一个问题是怎样提供共享内存;第二个是公共内存的互斥关系则是程序开发人员的责任。



5.信号量(semaphore):



主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。



6.套接字(socket);



这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。



linux下的进程间通信-详解



详细的讲述进程间通信在这里绝对是不可能的事情,而且笔者很难有信心说自己对这一部分内容的认识达到了什么样的地步,所以在这一节的开头首先向大家推荐著 名作者Richard Stevens的著名作品:《Advanced Programming in the UNIX Environment》,它的中文译本《UNIX环境高级编程》已有机械工业出版社出版,原文精彩,译文同样地道,如果你的确对在Linux下编程有浓 厚的兴趣,那么赶紧将这本书摆到你的书桌上或计算机旁边来。说这么多实在是难抑心中的景仰之情,言归正传,在这一节里,我们将介绍进程间通信最最初步和最 最简单的一些知识和概念。
   首先,进程间通信至少可以通过传送打开文件来实现,不同的进程通过一个或多个文件来传递信息,事实上,在很多应用系统里,都使用了这种方法。但一般说来, 进程间通信(IPC:InterProcess Communication)不包括这种似乎比较低级的通信方法。Unix系统中实现进程间通信的方法很多,而且不幸的是,极少方法能在所有的Unix系 统中进行移植(唯一一种是半双工的管道,这也是最原始的一种通信方式)。而Linux作为一种新兴的操作系统,几乎支持所有的Unix下常用的进程间通信 方法:管道、消息队列、共享内存、信号量、套接口等等。下面我们将逐一介绍。



   2.3.1 管道
   管道是进程间通信中最古老的方式,它包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信。
   无名管道由pipe()函数创建:
   #include
   int pipe(int filedis[2]);
   参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。下面的例子示范了如何在父进程和子进程间实现通信。



#define INPUT 0
#define OUTPUT 1



void main() {
int file_descriptors[2];
/定义子进程号 */
pid_t pid;
char buf[256];
int returned_count;
/
创建无名管道/
pipe(file_descriptors);
/
创建子进程/
if((pid = fork()) == -1) {
printf(“Error in fork\n”);
exit(1);
}
/
执行子进程/
if(pid == 0) {
printf(“in the spawned (child) process…\n”);
/
子进程向父进程写数据,关闭管道的读端/
close(file_descriptors[INPUT]);
write(file_descriptors[OUTPUT], “test data”, strlen(“test data”));
exit(0);
} else {
/
执行父进程/
printf(“in the spawning (parent) process…\n”);
/
父进程从管道读取子进程写的数据,关闭管道的写端/
close(file_descriptors[OUTPUT]);
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
printf(“%d bytes of data received from spawned process: %s\n”,
returned_count, buf);
}
}
   在Linux系统下,有名管道可由两种方式创建:命令行方式mknod系统调用和函数mkfifo。下面的两种途径都在当前目录下生成了一个名为myfifo的有名管道:
     方式一:mkfifo(“myfifo”,”rw”);
     方式二:mknod myfifo p
   生成了有名管道后,就可以使用一般的文件I/O函数如open、close、read、write等来对它进行操作。下面即是一个简单的例子,假设我们已经创建了一个名为myfifo的有名管道。
  /
进程一:读有名管道*/
#include
#include
void main() {
FILE * in_file;
int count = 1;
char buf[80];
in_file = fopen("mypipe", "r");
if (in_file == NULL) {
printf("Error in fdopen.\n");
exit(1);
}
while ((count = fread(buf, 1, 80, in_file)) > 0)
printf("received from pipe: %s\n", buf);
fclose(in_file);
}
  /* 进程二:写有名管道*/
#include
#include
void main() {
FILE * out_file;
int count = 1;
char buf[80];
out_file = fopen("mypipe", "w");
if (out_file == NULL) {
printf("Error opening pipe.");
exit(1);
}
sprintf(buf,"this is test data for the named pipe example\n");
fwrite(buf, 1, 80, out_file);
fclose(out_file);
}



   2.3.2 消息队列
   消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。
事实上,它是一种正逐渐被淘汰的通信方式,我们可以用流管道或者套接口的方式来取代它,所以,我们对此方式也不再解释,也建议读者忽略这种方式。



   2.3.3 共享内存
    共享内存是运行在同一台机器上的进程间通信最快的方式,因为数据不需要在不同的进程间复制。通常由一个进程创建一块共享内存区,其余进程对这块内存区进行 读写。得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的将是 实际的物理内存,在Linux系统下,这只有通过限制Linux系统存取的内存才可以做到,这当然不太实际。常用的方式是通过shmXXX函数族来实现利 用共享内存进行存储的。
   首先要用的函数是shmget,它获得一个共享存储标识符。



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



      int shmget(key_t key, int size, int flag);
    这个函数有点类似大家熟悉的malloc函数,系统按照请求分配size大小的内存用作共享内存。Linux系统内核中每个IPC结构都有的一个非负整数 的标识符,这样对一个消息队列发送消息时只要引用标识符就可以了。这个标识符是内核由IPC结构的关键字得到的,这个关键字,就是上面第一个函数的 key。数据类型key_t是在头文件sys/types.h中定义的,它是一个长整形的数据。在我们后面的章节中,还会碰到这个关键字。
  
当共享内存创建后,其余进程可以调用shmat()将其连接到自身的地址空间中。
   void *shmat(int shmid, void *addr, int flag);
   shmid为shmget函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址,进程可以对此进程进行读写操作。
    使用共享存储来实现进程间通信的注意点是对数据存取的同步,必须确保当一个进程去读取数据时,它所想要的数据已经写好了。通常,信号量被要来实现对共享存 储数据存取的同步,另外,可以通过使用shmctl函数设置共享存储内存的某些标志位如SHM_LOCK、SHM_UNLOCK等来实现。



   2.3.4 信号量
   信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是前一节的共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:
   (1) 测试控制该资源的信号量。
   (2) 若此信号量的值为正,则允许进行使用该资源。进程将信号量减1。
   (3) 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。
   (4) 当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
    维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/usr/src/linux/include /linux /sem.h 中看到内核用来维护信号量状态的各个结构的定义。信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获 得一个信号量ID。



struct sem {
short sempid;/* pid of last operaton /
ushort semval;/
current value /
ushort semncnt;/
num procs awaiting increase in semval /
ushort semzcnt;/
num procs awaiting semval = 0 */
}



   #include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/sem.h>
   int semget(key_t key, int nsems, int flag);



  key是前面讲过的IPC结构的关键字,flag将来决定是创建新的信号量集合,还是引用一个现有的信号量集合。nsems是该集合中的信号量数。如果是创建新 集合(一般在服务器中),则必须指定nsems;如果是引用一个现有的信号量集合(一般在客户机中)则将nsems指定为0。



   semctl函数用来对信号量进行操作。
   int semctl(int semid, int semnum, int cmd, union semun arg);
   不同的操作是通过cmd参数来实现的,在头文件sem.h中定义了7种不同的操作,实际编程时可以参照使用。
  
semop函数自动执行信号量集合上的操作数组。
   int semop(int semid, struct sembuf semoparray[], size_t nops);
   semoparray是一个指针,它指向一个信号量操作数组。nops规定该数组中操作的数量。



   下面,我们看一个具体的例子,它创建一个特定的IPC结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后我们清除信号量。在下面的代码中,函数ftok生成我们上文所说的唯一的IPC关键字。



#include
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
void main() {
key_t unique_key; /* 定义一个IPC关键字*/
int id;
struct sembuf lock_it;
union semun options;
int i;



unique_key = ftok(“.”, ‘a’); /* 生成关键字,字符’a’是一个随机种子/
/
创建一个新的信号量集合/
id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);
printf(“semaphore id=%d\n”, id);
options.val = 1; /
设置变量值/
semctl(id, 0, SETVAL, options); /
设置索引0的信号量*/



/打印出信号量的值/
i = semctl(id, 0, GETVAL, 0);
printf(“value of semaphore at index 0 is %d\n”, i);



/下面重新设置信号量/
lock_it.sem_num = 0; /设置哪个信号量/
lock_it.sem_op = -1; /定义操作/
lock_it.sem_flg = IPC_NOWAIT; /操作方式/
if (semop(id, &lock_it, 1) == -1) {
printf(“can not lock semaphore.\n”);
exit(1);
}



i = semctl(id, 0, GETVAL, 0);
printf(“value of semaphore at index 0 is %d\n”, i);



/清除信号量/
semctl(id, 0, IPC_RMID, 0);
}



semget()



可以使用系统调用semget()创建一个新的信号量集,或者存取一个已经存在的信号量集:



系统调用:semget();
原型:intsemget(key_t key,int nsems,int semflg);
返回值:如果成功,则返回信号量集的IPC标识符。如果失败,则返回-1:errno=EACCESS(没有权限)
EEXIST(信号量集已经存在,无法创建)
EIDRM(信号量集已经删除)
ENOENT(信号量集不存在,同时没有使用IPC_CREAT)
ENOMEM(没有足够的内存创建新的信号量集)
ENOSPC(超出限制)
系统调用semget()的第一个参数是关键字值(一般是由系统调用ftok()返回的)。系统内核将此值和系统中存在的其他的信号量集的关键字值进行比较。打开和存取操作与参数semflg中的内容相关。IPC_CREAT如果信号量集在系统内核中不存在,则创建信号量集。IPC_EXCL当和 IPC_CREAT一同使用时,如果信号量集已经存在,则调用失败。如果单独使用IPC_CREAT,则semget()要么返回新创建的信号量集的标识符,要么返回系统中已经存在的同样的关键字值的信号量的标识符。如果IPC_EXCL和IPC_CREAT一同使用,则要么返回新创建的信号量集的标识符,要么返回-1。IPC_EXCL单独使用没有意义。参数nsems指出了一个新的信号量集中应该创建的信号量的个数。信号量集中最多的信号量的个数是在linux/sem.h中定义的:
#defineSEMMSL32/<=512maxnumofsemaphoresperid/
下面是一个打开和创建信号量集的程序:
intopen_semaphore_set(key_t keyval,int numsems)
{
intsid;
if(!numsems)
return(-1);
if((sid=semget(mykey,numsems,IPC_CREAT|0660))==-1)
{
return(-1);
}
return(sid);
}
};
==============================================================
semop()



系统调用:semop();
调用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);
返回值:0,如果成功。-1,如果失败:errno=E2BIG(nsops大于最大的ops数目)
EACCESS(权限不够)
EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行)
EFAULT(sops指向的地址无效)
EIDRM(信号量集已经删除)
EINTR(当睡眠时接收到其他信号)
EINVAL(信号量集不存在,或者semid无效)
ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构)
ERANGE(信号量值超出范围)



第一个参数是关键字值。第二个参数是指向将要操作的数组的指针。第三个参数是数组中的操作的个数。参数sops指向由sembuf组成的数组。此数组是在linux/sem.h中定义的:
/semop systemcall takes an array of these/
structsembuf{
ushortsem_num;/semaphore index in array/
shortsem_op;/semaphore operation/
shortsem_flg;/operation flags/
sem_num将要处理的信号量的个数。
sem_op要执行的操作。
sem_flg操作标志。
如果sem_op是负数,那么信号量将减去它的值。这和信号量控制的资源有关。如果没有使用IPC_NOWAIT,那么调用进程将进入睡眠状态,直到信号量控制的资源可以使用为止。如果sem_op是正数,则信号量加上它的值。这也就是进程释放信号量控制的资源。最后,如果sem_op是0,那么调用进程将调用sleep(),直到信号量的值为0。这在一个进程等待完全空闲的资源时使用。
===============================================================
semctl()



系统调用:semctl();
原型:int semctl(int semid,int semnum,int cmd,union semunarg);
返回值:如果成功,则为一个正数。
如果失败,则为-1:errno=EACCESS(权限不够)
EFAULT(arg指向的地址无效)
EIDRM(信号量集已经删除)
EINVAL(信号量集不存在,或者semid无效)
EPERM(EUID没有cmd的权利)
ERANGE(信号量值超出范围)



系统调用semctl用来执行在信号量集上的控制操作。这和在消息队列中的系统调用msgctl是十分相似的。但这两个系统调用的参数略有不同。因为信号量一般是作为一个信号量集使用的,而不是一个单独的信号量。所以在信号量集的操作中,不但要知道IPC关键字值,也要知道信号量集中的具体的信号量。这两个系统调用都使用了参数cmd,它用来指出要操作的具体命令。两个系统调用中的最后一个参数也不一样。在系统调用msgctl中,最后一个参数是指向内核中使用的数据结构的指针。我们使用此数据结构来取得有关消息队列的一些信息,以及设置或者改变队列的存取权限和使用者。但在信号量中支持额外的可选的命令,这样就要求有一个更为复杂的数据结构。
系统调用semctl()的第一个参数是关键字值。第二个参数是信号量数目。
参数cmd中可以使用的命令如下:
·IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
·IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
·IPC_RMID将信号量集从内存中删除。
·GETALL用于读取信号量集中的所有信号量的值。
·GETNCNT返回正在等待资源的进程数目。
·GETPID返回最后一个执行semop操作的进程的PID。
·GETVAL返回信号量集中的一个单个的信号量的值。
·GETZCNT返回这在等待完全空闲的资源的进程数目。
·SETALL设置信号量集中的所有的信号量的值。
·SETVAL设置信号量集中的一个单独的信号量的值。
参数arg代表一个semun的实例。semun是在linux/sem.h中定义的:
/arg for semctl systemcalls./
unionsemun{
intval;/value for SETVAL/
structsemid_dsbuf;/buffer for IPC_STAT&IPC_SET/
ushort
array;/array for GETALL&SETALL/
structseminfo__buf;/buffer for IPC_INFO/
void
__pad;
val当执行SETVAL命令时使用。buf在IPC_STAT/IPC_SET命令中使用。代表了内核中使用的信号量的数据结构。array在使用GETALL/SETALL命令时使用的指针。
下面的程序返回信号量的值。当使用GETVAL命令时,调用中的最后一个参数被忽略:
intget_sem_val(intsid,intsemnum)
{
return(semctl(sid,semnum,GETVAL,0));
}
下面是一个实际应用的例子:
#defineMAX_PRINTERS5
printer_usage()
{
int x;
for(x=0;x<MAX_PRINTERS;x++)
printf(“Printer%d:%d\n\r”,x,get_sem_val(sid,x));
}
下面的程序可以用来初始化一个新的信号量值:
void init_semaphore(int sid,int semnum,int initval)
{
union semunsemopts;
semopts.val=initval;
semctl(sid,semnum,SETVAL,semopts);
}
注意系统调用semctl中的最后一个参数是一个联合类型的副本,而不是一个指向联合类型的指针。



   2.3.5 套接口
    套接口(socket)编程是实现Linux系统和其他大多数操作系统中进程间通信的主要方式之一。我们熟知的WWW服务、FTP服务、TELNET服务 等都是基于套接口编程来实现的。除了在异地的计算机进程间以外,套接口同样适用于本地同一台计算机内部的进程间通信。关于套接口的经典教材同样是 Richard Stevens编著的《Unix网络编程:联网的API和套接字》,清华大学出版社出版了该书的影印版。它同样是Linux程序员的必备书籍之一。
    关于这一部分的内容,可以参照本文作者的另一篇文章《设计自己的网络蚂蚁》,那里由常用的几个套接口函数的介绍和示例程序。这一部分或许是Linux进程 间通信编程中最须关注和最吸引人的一部分,毕竟,Internet 正在我们身边以不可思议的速度发展着,如果一个程序员在设计编写他下一个程序的时候,根本没有考虑到网络,考虑到Internet,那么,可以说,他的设 计很难成功。



3 Linux的进程和Win32的进程/线程比较
   熟悉WIN32编程的人一定知道,WIN32的进程管理方式与Linux上有着很大区别,在UNIX里,只有进程的概念,但在WIN32里却还有一个”线程”的概念,那么Linux和WIN32在这里究竟有着什么区别呢?
    WIN32里的进程/线程是继承自OS/2的。在WIN32里,”进程”是指一个程序,而”线程”是一个”进程”里的一个执行”线索”。从核心上讲, WIN32的多进程与Linux并无多大的区别,在WIN32里的线程才相当于Linux的进程,是一个实际正在执行的代码。但是,WIN32里同一个进 程里各个线程之间是共享数据段的。这才是与Linux的进程最大的不同。
   下面这段程序显示了WIN32下一个进程如何启动一个线程。



int g;
DWORD WINAPI ChildProcess( LPVOID lpParameter ){
int i;
for ( i = 1; i <1000; i ++) {
g ++;
printf( “This is Child Thread: %d\n”, g );
}
ExitThread( 0 );
};



void main()
{
int threadID;
int i;
g = 0;
CreateThread( NULL, 0, ChildProcess, NULL, 0, &threadID );
for ( i = 1; i <1000; i ++) {
g ++;
printf( “This is Parent Thread: %d\n”, g );
}
}



    在WIN32下,使用CreateThread函数创建线程,与Linux下创建进程不同,WIN32线程不是从创建处开始运行的,而是由 CreateThread指定一个函数,线程就从那个函数处开始运行。此程序同前面的UNIX程序一样,由两个线程各打印1000条信息。 threadID是子线程的线程号,另外,全局变量g是子线程与父线程共享的,这就是与Linux最大的不同之处。大家可以看出,WIN32的进程/线程 要比Linux复杂,在Linux要实现类似WIN32的线程并不难,只要fork以后,让子进程调用ThreadProc函数,并且为全局变量开设共享 数据区就行了,但在WIN32下就无法实现类似fork的功能了。所以现在WIN32下的C语言编译器所提供的库函数虽然已经能兼容大多数 Linux/UNIX的库函数,但却仍无法实现fork。
   对于多任务系统,共享数据区是必要的,但也是一个容易引起混乱的问题,在WIN32下,一个程序员很容易忘记线程之间的数据是共享的这一情况,一个线程修 改过一个变量后,另一个线程却又修改了它,结果引起程序出问题。但在Linux下,由于变量本来并不共享,而由程序员来显式地指定要共享的数据,使程序变 得更清晰与安全。
至于WIN32的”进程”概念,其含义则是”应用程序”,也就是相当于UNIX下的exec了。
   Linux也有自己的多线程函数pthread,它既不同于Linux的进程,也不同于WIN32下的进程,关于pthread的介绍和如何在Linux环境下编写多线程程序我们将在另一篇文章《Linux下的多线程编程》中讲述。
  
  
  


Category linux