Unix IPC进程间通信

来源:互联网 发布:多益网络找回 编辑:程序博客网 时间:2024/04/28 01:03

主要手段

    1.管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信; 有名管道克服了管道没有名字的限制,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

    2.信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身。

    3.消息队列:消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点

    4.共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

    5.信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

    6.套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。

 

一.管道 (Windows管道)

        管道是Linux支持的最初Unix IPC形式之一,特点:

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

       2.只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

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

 

1.1管道的创建

       管道是由调用pipe函数而创建的。

名称:

pipe

功能:

创建管道

头文件:

#include <unistd.h>

函数原形:

int pipe(int filedes[2]);

参数:

 

返回值:

若成功返回0,若出错则返回-1

    

 

 

 

 

 

经由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。


1:     2: #include <stdio.h>   3: #include <stdlib.h>   4: #include <unistd.h>   5: #include <sys/types.h>   6:     7: int main()   8: {   9: int n;  10: int fd[2];  11: pid_t pid;  12: char line[1024];  13:    14: printf("printf twice. why?");  15: /*printf("have \\n, printf once.");*/  16:    17: if(pipe(fd)<0) /*建立管道*/  18:     perror("pipe error");  19: if((pid=fork())<0) /*创建子进程*/  20:     perror("fork error");  21: else if(pid==0) /*如果是子进父程*/  22: {  23:     char info[] = "[From Child To Father By Pipe] I'm child,hello father!\n";  24:     printf("[Child process pid: %d]\n", getpid());  25:     close(fd[0]); /*关闭不用的读描述符*/  26:     write(fd[1],info,sizeof(info));/*往管道里写数据*/  27: }  28: else  /*如果是父进程*/  29: {  30:     printf("[Father process pid %d] returned pid:%d(is child pid)\n", getpid(),pid);  31:     close(fd[1]); /*关闭不用的写描述符*/  32:     wait(); /*等待子进程结束*/  33:     n=read(fd[0],line,1024); /*从管道里读数据,读到缓冲数组中*/  34:     write(STDOUT_FILENO,line,n); /*把缓冲区的数据写道屏幕上*/  35: }  36:    37: exit(0);  38:    39: }  40:    41: /*  42: run Result:  43:     printf twice. why?[Child process pid: 22219]  44:     printf twice. why?[Father process pid 22218] returned pid:22219(is child pid)  45:     [From Child To Father By Pipe] I'm child,hello father!  46: */


1.2管道的读写规则

     

管道两端可分别用描述字fd[0]以及fd[1]来描述,一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。一般文件的I/O函数都可以用于管道,如close、read、write等等。

       从管道中读取数据:如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0; 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,否则返回你请求的字节数。注意的是,读出的字节数是你Write设置的大小。

       向管道中写入数据:向管道中写入数据时,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。

 

1.3管道的局限性

只支持单向数据流;

只能用于具有亲缘关系的进程之间;

没有名字;

缓冲区有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);

传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等

 

二.有名管道

       有名管道(named pipeFIFO)不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信,因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

1.有名管道的创建

       mkfifo可以重建一个有名管道。

名称:

mkfifo

功能:

创建有名管道

头文件:

#include <sys/types.h>

#include <sys/stat.h>

函数原形:

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

参数:

 

返回值:

若成功则为0,若出错则为-1

     

 

 

 

 

 

 

第一个参数是一个普通的路径名,也就是创建后FIFO的名字。

第二个参数与打开普通文件的open()函数中的mode 参数相同。

如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误。

一般文件的I/O函数都可以用于FIFO,如closereadwrite等等。

 

下面是不同祖先通过管道通信的例子。

/* service.c*/#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdlib.h>#include <stdio.h>  #define MAXLIME 1000 int main(int argc,char *argv[]) {FILE *fd;char buf[MAXLIME]; /*定义缓冲区*/ if(mkfifo(argv[1],S_IRUSR | S_IWUSR)<0) /*创建有名管道, argv[1]为文件名,会自动创建*/    perror("mkfifo erro");else    perror("mkfilo success");if((fd=fopen(argv[1],"a+rw"))==NULL) /*打开刚创建的有名管道*/    perror("open error");else    perror("open pipe success"); fgets(buf,MAXLIME,fd); /*阻塞,从有名管道读取字符流*/ printf("server:%s",buf); exit(0); } /*client.c*/#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdlib.h>#include <stdio.h>#define MAXLIME 1000 int main(int argc,char *argv[]){FILE *fd;char buf[MAXLIME];  /*定义缓冲区*/ if((fd=fopen(argv[1],"a+rw"))==NULL) /*打开由服务程序创建的有名管道*/    perror("open error"); printf("client:"); fgets(buf,MAXLIME,stdin); /*把用户输入存入缓冲区*/ fputs(buf,fd); /*把缓冲区内容输入有名管道*/ exit(0); }




复杂一些例子:

有名管道可以用于任何两个程序间通信,因为有名字可引用。注意管道都是单向的,因此双方通信需要两个管道。下面分别是这两个程序代码,同样是Lucy先运行,然后是Peter。fifoLucy.c#include<sys/types.h>#include<sys/stat.h>#include<stdio.h>#include<errno.h>#include<fcntl.h>#include<string.h>#include<unistd.h>#include<stdlib.h>int main(){ char write_fifo_name[] = "lucy";   char read_fifo_name[] = "peter";   int write_fd, read_fd;   char buf[256];   int len;   struct stat stat_buf;   int ret = mkfifo(write_fifo_name, S_IRUSR | S_IWUSR);   if( ret == -1)  {    printf("Fail to create FIFO %s: %s",write_fifo_name,strerror(errno));      exit(-1);        }   write_fd = open(write_fifo_name, O_WRONLY);   if(write_fd == -1)  {    printf("Fail to open FIFO %s: %s",write_fifo_name,strerror(errno));      exit(-1);        }           while((read_fd = open(read_fifo_name,O_RDONLY)) == -1)  {    sleep(1);        }       while(1)  {      printf("Lucy: ");    fgets(buf, 256, stdin);      buf[strlen(buf)-1] = '\0';      if(strncmp(buf,"quit", 4) == 0)   {         close(write_fd);         unlink(write_fifo_name);         close(read_fd);         exit(0);                }      write(write_fd, buf, strlen(buf));      len = read(read_fd, buf, 256);      if( len > 0)   {       buf[len] = '\0';         printf("Peter: %s\n", buf);                }        }}fifoPeter.c#include<sys/types.h>#include<sys/stat.h>#include<string.h>#include<stdio.h>#include<errno.h>#include<fcntl.h>#include<stdlib.h>int main(void){ char write_fifo_name[] = "peter";   char read_fifo_name[] = "lucy";   int write_fd, read_fd;   char buf[256];   int len;   int ret = mkfifo(write_fifo_name, S_IRUSR | S_IWUSR);   if( ret == -1)  {    printf("Fail to create FIFO %s: %s",write_fifo_name,strerror(errno));    exit(-1);    }   while((read_fd = open(read_fifo_name, O_RDONLY)) == -1)  {  sleep(1);        }           write_fd = open(write_fifo_name, O_WRONLY);   if(write_fd == -1)  {    printf("Fail to open FIFO %s: %s", write_fifo_name, strerror(errno));      exit(-1);        }   while(1)  {      len = read(read_fd, buf, 256);      if(len > 0)   {       buf[len] = '\0';         printf("Lucy: %s\n",buf);                }      printf("Peter: ");      fgets(buf, 256, stdin);      buf[strlen(buf)-1] = '\0';      if(strncmp(buf,"quit", 4) == 0)   {       close(write_fd);       unlink(write_fifo_name);         close(read_fd);         exit(0);                }      write(write_fd, buf, strlen(buf));        }}



2.有名管道的打开规则

       如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。

       如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

 

3.有名管道的读写规则

       如果有进程读打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。

       对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。

        如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。

 

对于设置了阻塞标志的写操作:

       当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。

       当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

对于没有设置阻塞标志的写操作:

       当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

       当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。

 

 

三.信号

       信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。

信号 上

信号 下

 

四.XSI IPC

下面的三种进程间通信方式,即消息队列,信号量以及共享内存,它们之间有很多相似之处。我们统一称它们为XSI  IPC。

相似之处如下:

 

1.标识符和键

       每个XSI IPC都用一个非负整数的标识符(identifier)加以引用。例如,为了对一个消息队列发送或取消消息,只须知道其队列标识符。与文件标识符不同,IPC标识符不是小的整数。当一个IPC结构被创建,以后又被删除,与这种结构相关的标识符连续加1,直至达到一个整数的最大正值,然后又回转到0

       标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上会合,需要提供一个外部名方案。为此使用了键(key,每个IPC对象都与一个键相关联,于是键就用为该对象的外部名。

       无论何时创建IPC结构,都应指定一个键,键的数据类型是基本系统数据类型key_t,通常在头文件<sys/types.h>中被定义为长整形。键由内核变换成标识符。

       我们用ftok函数创建一个键。

名称:

ftok

功能:

创建一个键

头文件:

#include <sys/ipc.h>

# include <sys/types.h>

函数原形:

key_t ftok(const char *path,int id);

参数:

path

id

返回值:

若成功返回键,若出错返回(key_t-1

 

 

 

 

 

 

 

 

 

path必须引用一个现存文件。当产生键时,只使用id参数的低8位。

1pathname一定要在系统中存在

2pathname一定是使用进程能够访问的

3proj_id是一个1255之间的一个整数值,典型的值是一个ASCII值。

 

2.权限结构

       XSI IPC 为每一个IPC结构设置了一个ipc_perm结构。该结构规定了权限和所有者。

struct ipc_perm

{ ushort uid; /*当前用户*/

ushort gid; /*当前进程组*/

ushort cuid; /*创建用户*/

ushort cgid; /*创建进程组*/

ushort mode; /*存取许可权*/

{ short pid1; long pad2;} /*由系统使用*/

}

其中mode权限位设置如下表

 

权限

用户读

用户写

0400

0200

组读

组写

0040

0020

其他读

其他写

0004

0002

 

       在创建XSI IPC结构时,对所有字段都赋初值。可以调用msgctlsemctlshmctl修改uid,gidmode字段。

System V 消息队列

         

1、基本概念

       消息队列就是一个消息的链表。有足够写权限的进程可往队列中放置消息,有足够读权限的进程可从队列中取走消息。每个消息是一个记录它由发送者赋予一个优先级。在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。这跟管道和FIFO是相反的,对后者来说,除非读出者已存在,否则先有写入者是没有意义的。消息队列是随内核持续的。一个进程可以先往某个队列写入一些消息后终止,让另外一个进程在以后某个时刻读出这些消息。这一点管道和FIFO是不支持的。

 

2、分类

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

 

3、和消息队列相关的结构

       每个消息队列都有一个队列头,用结构struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。读者可以访问这些信息,也可以设置其中的某些信息。这个结构存于系统空间。

struct msg_queue {

    struct kern_ipc_perm q_perm;

    time_t q_stime;         /* last msgsnd time */

    time_t q_rtime;         /* last msgrcv time */

    time_t q_ctime;         /* last change time */

    unsigned long q_cbytes;     /* current number of bytes on queue */

    unsigned long q_qnum;       /* number of messages in queue */

    unsigned long q_qbytes;     /* max number of bytes on queue */

    pid_t q_lspid;          /* pid of last msgsnd */

    pid_t q_lrpid;          /* last receive pid */

    struct list_head q_messages;

    struct list_head q_receivers;

    struct list_head q_senders;

};

 

结构msqid_ds用来设置或返回消息队列的信息,存在于用户空间;

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

 

4 打开或创建消息队列

       消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需提供该消息队列的键值即可。msgget用于创建一个消息队列或打开一个现存的队列。

1

名称:

msgget

功能:

创建消息队列

头文件:

#include <sys/types.h>

#include <sys/msg.h>

#inlcude <sys/ipc.h>

函数原形:

int msgget(key_t key,int msgflag);

参数:

key 消息队列的键

flag 一些标志位

返回值:

若成功则为消息队列描述字若出错则为-1

     

 

 

 

 

 

 

 

 

参数key是一个键值,由ftok获得;msgflg参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。

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

       1.如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;

       2key参数为IPC_PRIVATE

       参数msgflg可以为以下:IPC_CREAT(创建消息队列)、IPC_EXCL  )、IPC_NOWAIT  )或三者的或结果。

       还有注意的是:当创建一个新队列时,系统自动初始化struct msqid_ds结构的下列成员。

ipc_perm结构按我们以前说的进行初始化。该结构中mode成员按flag中的相应权限位设置。

msg_qnum,msg_lspid,msg_lrpid,msg_stime,msg_rtime都设置为0

msg_ctime设置为当前时间。

msg_qbytes设置为系统限制值。

 

5、获得和修改消息队列属性,删除消息队列

2.

名称:

msgctl

功能:

对消息队列进行多种操作

头文件:

#include <sys/msg.h>

函数原形:

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

参数:

msqid   消息队列描述字

cmd    要执行的操作

buf     此队列的struct msqid_ds结构

返回值:

若成功返回0,若出错返回-1

     

 

 

 

 

 

 

 

该系统调用对由msqid标识的消息队列执行cmd操作,共有三种cmd操作:IPC_STATIPC_SETIPC_RMID

IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid_da结构中;

IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid_ds结构中;可设置属性包括:msg_perm.uidmsg_perm.gidmsg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。

IPC_RMID:删除msqid_ds标识的消息队列.

 

6、用消息队列发送和接收消息

3

名称:

msgsnd

功能:

将数据放到消息队列上

头文件:

#include <sys/types.h>

#include <sys/msg.h>

#inlcude <sys/ipc.h>

函数原形:

int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);

参数:

msqid   消息队列描述字

msgp       指向消息数据的指针

msgsz    发送消息的大小

msgflg       标志位

返回值:

若成功则为0,若出错则为-1

     

 

 

 

 

 

 

 

 

 

msgid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中,消息的大小由msgze指定。

       struct msgbuf{

              long mtype; /*消息类型*/

              char mtext[1]; /*消息数据*/

       };

       我们可以把msgbuf结构看成是一个模版,程序员可以根据自己的需要来设计直接的消息结构。举例来说,如果某个应用需要交换由一个整数后跟一个8字节字符数组构成的消息,那它可以如下定义自己的结构:

       typedef struct my_msgbuf{

              long mtypel

              int    mshort;

              char mchar[MY_DATA]

       }Message;

       对发送消息来说,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。造成msgsnd()等待的条件有两种:

       1.当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;

       2.当前消息队列的消息数(单位"")不小于消息队列的总容量(单位"字节数"),此时,虽然消息队列中的消息数目很多,但基本上都只有一个字节。

       msgsnd()解除阻塞的条件有三个:

       1.不满足上述两个条件,即消息队列中有容纳该消息的空间;

       2msqid代表的消息队列被删除;

      3.调用msgsnd()的进程被信号中断;

       msgsnd成功返回,与消息队列相关的msqid_ds结构得到更新,以标明发出该调用的进程IDmsg_lspid),进行该调用的时间(msg_stime),并指示队列中增加了一条消息。

 

4

名称:

msgrcv

功能:

从队列中取出消息

头文件:

#include <sys/types.h>

#include <sys/msg.h>

#inlcude <sys/ipc.h>

函数原形:

int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);

参数:

msqid   消息队列描述字

msgp    指向消息数据的指针

msgsz   接收消息的大小

msgtyp   请求读取的消息类型

msgflg   标志位

返回值:

若成功则为消息数据部分的长度,若出错则为-1

      

 

 

 

 

 

 

 

 

 

 

 

 

该系统调用从msgid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。

       msqid为消息队列描述字;消息返回后存储在msgp指向的地址,msgsz指定msgbufmtext成员的长度(即消息内容的长度),msgtyp为请求读取的消息类型;读消息标志msgflg可以为以下几个常值的或:

       IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG

       IPC_EXCEPT msgtyp>0配合使用,返回队列中第一个类型不为msgtyp的消息

       IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失。

       msgtyp使我们可以指定想要哪一种消息:

       msgtyp==0     返回队列中的第一个消息。

       msgtyp>0       返回队列中消息类型为msgtyp的第一个消息。

       msgtyp<0       返回队列中消息类型值小于或等于msgtyp绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。

       msgrcv成功返回,与消息队列相关的msqid_ds结构得到更新,以标明发出该调用的进程IDmsg_lspid),进行该调用的时间(msg_stime),并将队列中消息数(msg_qnum)减1

 

7.消息队列与管道以及有名管道的比较

       消息队列与管道以及有名管道相比,具有更大的灵活性,首先,它提供有格式字节流,有利于减少开发人员的工作量;其次,消息具有类型,在实际应用中,可作为优先级使用。这两点是管道以及有名管道所不能比的。同样,消息队列可以在几个进程间复用,而不管这几个进程是否具有亲缘关系,这一点与有名管道很相似;但消息队列是随内核持续的,与有名管道(随进程持续)相比,生命力更强,应用空间更大。

       输出消息队列信息的例子:

#include <sys/types.h> #include <sys/msg.h>#include <sys/ipc.h>#include <stdio.h>#define KEY 75void msg_stat(int,struct msqid_ds);int main(){int msgid;int sflags,rflags;struct msqid_ds msg; struct msgbuf{    int mtypes;    char mtext[10];}msg_buf; msgid=msgget(KEY,IPC_CREAT);msg_stat(msgid,msg);sflags=IPC_NOWAIT;msg_buf.mtypes=10;msg_buf.mtext[0]='a';msgsnd(msgid,&msg_buf,sizeof(msg_buf.mtext),sflags);msg_stat(msgid,msg); rflags=IPC_NOWAIT|MSG_NOERROR;msgrcv(msgid,&msg_buf,4,10,rflags); msg.msg_perm.uid=8;msg.msg_perm.gid=8;msg.msg_qbytes=16388;msgctl(msgid,IPC_SET,&msg);msg_stat(msgid,msg);msgctl(msgid,IPC_RMID,NULL);}void msg_stat(int msgid,struct msqid_ds msg_info){    msgctl(msgid,IPC_STAT,&msg_info);printf("current number of bytes on queue is %d\n",msg_info.msg_cbytes); /*最后消息的类型*/printf("number of messages in queue is %d\n",msg_info.msg_qnum);/*当前消息队列的消息数*/ printf("max number of bytes on queue is %d\n",msg_info.msg_qbytes); /*消息队列的容量*/printf("pid of last msgsnd is %d\n",msg_info.msg_lspid);/*最后发送消息时间*/printf("pid of last msgrcv is %d\n",msg_info.msg_lrpid); /*最后接受消息进程*/printf("last msgsnd time is %s", ctime(&(msg_info.msg_stime)));/*最后发送消息时间*/printf("last msgrcv time is %s", ctime(&(msg_info.msg_rtime)));/*最后接受消息时间*/printf("last change time is %s", ctime(&(msg_info.msg_ctime)));/* 最后修改时间*/printf("msg uid is %d\n",msg_info.msg_perm.uid); /*进程id*/printf("msg gid is %d\n",msg_info.msg_perm.gid); /*进程组id*/} 


 

用消息队列实现进程间通信的例子:

/* client.c*/#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdlib.h>#include <stdio.h>#include <sys/msg.h> #include <sys/ipc.h> #define MSGKEY 75#define MAXLINE 100struct msgform /*定义消息格式*/ {long mtype; /*消息类型*/pid_t pid; /*消息发送进程ID */ char mtext[100];/*消息数据*/ }msg; int msgqid;/*定义消息的id*/ void client() {     int i;     msgqid=msgget(MSGKEY,0777); /*打开75#消息队列*/printf("msgqid:%d\n",msgqid);     for(i=1;i<=5;i++)/*循环发送5个消息*/    {         msg.mtype=i;         printf("client:");        fgets(msg.mtext,MAXLINE,stdin);/*把用户输入读入缓冲区*/         msgsnd(msgqid,&msg,sizeof(msg),0); /*发送消息*/     }     exit(0); } main( ) {     client( ); } /*server.c*/#include <sys/types.h> #include <sys/msg.h> #include <sys/ipc.h> #define MSGKEY 75 struct msgform {long mtype;pid_t pid; char mtext[100]; }msg; int msgqid; void server( ) {     msgqid=msgget(MSGKEY,0777|IPC_CREAT); /*创建75#消息队列*/     printf("msgqid:%d\n",msgqid);    do {         msgrcv(msgqid,&msg,sizeof(msg),0,0); /*接收消息*/         printf("msg queue id:%d,type:%d,mtext:%s\n",msg.pid,msg.mtype,msg.mtext); /*输出消息*/     } while(msg.mtype!=5);     msgctl(msgqid,IPC_RMID,0); /*删除消息队列,归还资源*/     exit(0); } main( ) {     server( ); } 

   

 程序client.c和server.c,分别用于消息的发送与接收.server建立一个 Key 为75的消息队列,等待其它进程发来的消息。当遇到类型为1的消息,则作为结束信号,取消该队列,并退出server。server每接收到一个消息后显示一句“server:输入信息”. client使用 key为75的消息队列,先后发送类型从3到1的消息,然后退出。最后一个消息,即是 server端需要的结束信号。client 每发送一条消息后显示一句“client:要发送的消息”。 
    注意: 二个程序分别编辑、编译为client与server。执行: 
./server& 
./client

 

SystemV信号灯

 

一、概念

信号灯(也叫信号量)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。信号灯是进程/线程同步的一种方式,有时候我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关;有时候需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号灯。信号灯开关是二进制信号灯的一种逻辑扩展,两者实际调用的函数都是一样的。

信号灯分为以下三种。

一、System V信号灯,在内核中维护,可用于进程或线程间的同步,常用于进程的同步。

二、Posix有名信号灯,一种来源于POSIX技术规范的实时扩展方案(POSIX Realtime Extension,可用于进程或线程间的同步,但常用于线程。

三、Posix基于内存的信号灯,存放在共享内存区中,可用于进程或线程间的同步。

 

这里只介绍Sysem V信号灯。

为了获得共享资源进程需要执行下列操作:

(1)       测试控制该资源的信号灯。

(2)       若此信号灯的值为正,则进程可以使用该资源。进程信号灯值减1,表示它使用了一个资源单位。此进程使用完共享资源后对应的信号灯会加1。以便其他进程使用。

(3)       若此信号灯的值为0,则进程进入休息状态,直至信号灯值大于0。进程被唤醒后,它返回至第(1)步。

       为了正确地实现信号灯,信号灯值的测试值的测试及减1操作应当是原子操作。为此信号灯通常是在内核中实现的。

 

System V信号灯

       System V每一个信号灯函数都能对成组的通用信号量进行操作,这在一个进程需要锁定多个资源的时候是很容易办到的。

 

       与消息队列相似,信号灯也有一个结构semid_ds用于设置信号灯的信息。

       struct semid_ds{

              struct ipc_perm      sem_perm;     /*设置权限和所有者*/

              struct sem             *sem_base;   /*描述一个信号灯集中的每个信号灯的结构*/

              unsigned short              sem_nsems;   /*信号灯集中信号灯的数量*/

              time_t                   sem_otime;    

              time_t                   sem_ctime;  

              …….

       };

 

sem_perm结构的uidcuid成员被置为调用进程的有效用户IDgidcgid成员被置为调用进程的有效组ID

       sem_otime被置为0sem_ctime则被置为当前时间。

       sem_nsems被置为nsems参数的值。

 

       sem结构是内核用于维护某个给定信号灯的一组值的内部数据结构。一个信号灯集的每个成员由下面的结构描述:

struct sem{

              unsigned short_t semval;       /*信号灯的值*/

              short sempid;     /*对信号灯最后调用semop函数的进程ID*/

              unsigned short_t semncnt;  /*等待其值增长的进程数*/

              unsigned short_t semzcnt;  /*等待其值变为0的进程数*/

};

 

       内核除维护一个信号灯集中每个信号灯的实际值之外,内核还给该集合中每个信号灯维护另外三个信息:对其值执行最后一次操作的进程的进程ID、等待其值增长的进程数计数以及等待其值变为0的进程数计数。

 

二、信号灯函数

8

名称:

semget

功能:

获得一个信号灯id

头文件:

#include <sys/types.h>

#include <sys/ipc.h>

#inlcude <sys/sem.h>

函数原形:

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

参数:

key       

nsems     信号灯数

flag       选项

返回值:

若成功信号灯id,若出错返回-1

     

 

 

 

 

 

 

 

 

 

       

key是一个建我们可以用ftok函数来获得。key是一个整数值,不相关的进程将通过这个值去访问同一信号量。程序对任何信号量的访问都必须间接地进行,先由程序提供一个键字,再由系统生成一个相应的信号量标识码。

semget用于获得一个信号灯ID,同msg相似此函数也需要提供一个唯一的外部键,作为信号灯的外部标识。

       nsems参数是需要使用的信号量个数。如果是创建新集合,则必须制定nsems。如果引用一个现存的集合,则将nsems指定为0。一旦创建完毕一个信号灯集,我们就不能改变其中的信号灯数。

       oflag参数是一组标志,其作用与open函数的各种标志很相似。它低端的九个位是该信号量的权限,其作用相当于文件的访问权限。但它们可以与键值IPC_CREAT做按位的或操作以创建一个新的信号量。即使在设置了IPC_CREAT标志后给出的是一个现有的信号量的键字,也并不是一个错误。我们也可以通过IPC_CREATIPC_EXCL标志的联合使用确保自己将创建出一个新的独一无二的信号量来,如果该信号量已经存在,就会返回一个错误。

       当实际操作为创建一个信号灯时,相应的semid_ds结构的以下成员将被初始化:

与该集合中每个信号灯关联的各个sem结构并不初始化。这些结构是在以SET_VALSETALL命令调用semctl时初始化的。

  

主进程循环创建5个子进程,然后使它们去争取值为3的信号灯。没有争到的循环继续争,直到争到为止。

        
#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <errno.h> union semun{int val; struct semid_ds *buf;unsigned short int *array;};void print(int); /*表示共享资源*/int value; /*用于取得信号灯的值*/int main(int argc,char **argv){int n=0; /*用于循环*/int id; /*信号灯*/struct sembuf lock_it;union semun options;/*创建信号灯*/id = semget(ftok(argv[1],0),1,IPC_CREAT|IPC_EXCL|0666);if(id==-1)      perror("semget fail");options.val=3;/*设置信号灯的值*/semctl(id,0,SETVAL,options);while(n++<5) {    if(fork()==0) /*创建进程*/    {      while(1)        {          lock_it.sem_num=0;           lock_it.sem_op=-1; /*信号灯减1*/          lock_it.sem_flg=IPC_NOWAIT;           if((semop(id,&lock_it,1))==-1)             {              if(errno==EAGAIN) /*如果没有可用的信号灯就继续循环*/                    continue;               exit(1);              }           print(id); /*执行表示共享资源的函数*/            }      }   }         sleep(3);     semctl(id,0,IPC_RMID,0); /*删除一个信号灯*/    exit(0);} void print(int id){printf("I get id,my pid is %d\n",getpid());value=semctl(id,0,GETVAL);printf("now the value have %d\n",value);sleep(1);}

System V 共享内存区

 

一、什么是共享内存区

共享内存区是最快的可用IPC形式。它允许多个不相关的进程去访问同一部分逻辑内存。如果需要在两个运行中的进程之间传输数据,共享内存将是一种效率极高的解决方案。一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传输就不再涉及内核。这样就可以减少系统调用时间,提高程序效率。

共享内存是由IPC为一个进程创建的一个特殊的地址范围,它将出现在进程的地址空间中。其他进程可以把同一段共享内存段“连接到”它们自己的地址空间里去。所有进程都可以访问共享内存中的地址。如果一个进程向这段共享内存写了数据,所做的改动会立刻被有访问同一段共享内存的其他进程看到。

要注意的是共享内存本身没有提供任何同步功能。也就是说,在第一个进程结束对共享内存的写操作之前,并没有什么自动功能能够预防第二个进程开始对它进行读操作。共享内存的访问同步问题必须由程序员负责。可选的同步方式有互斥锁、条件变量、读写锁、纪录锁、信号灯。

       共享内存区分为System V共享内存区和Posix共享内存区。本节介绍System V共享内存区。

 

二、共享内存区结构

       和其他XSI IPC一样,内核为每个共享存储段设置一个shmid_ds结构。

       struct shmid_ds{

              struct ipc_perm      shm_perm;     /*operation perms*/

              int                         shm_segez;     /*size of segment*/

              time_t                   shm_atime;     /*last attach time*/

              time_t                   shm_dtime;     /*last detach time*/

              time_t                   shm_ctime;     /*last change time*/

              unsigned short              shm_lpid;       /*pid of creator*/

              unsigned short       shm_cpid;      /*pid of last operator*/

              short                     shm_nattch;    /*no.of current attaches*/

…….

       };

其中ipc_perm是我们在XSI IPC里介绍的权限结构。

struct ipc_perm{

       key_t      key;

       ushort     uid;  /*owner euid and egid*/

       ushort     gid;

       ushort     cuid; /*creator euid and egid*/

       ushort     cgid;

       ushort     mode; /*lower 9 bits of shmflg*/

       ushort     seq;  /*sequence number*/

};

 

三、共享内存区函数

       shmget函数创建一个尚未存在的共享内存区,或者访问一个已存在的共享内存区。

1

名称:

shmget

功能:

获得一个共享存储标识符

头文件:

#inlcude <sys/shm.h>

#include <sys/ipc.h>

函数原形:

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

参数:

ket 

size 内存区大小

shmflg 权限值

返回值:

若成功则返回共享内存id,若出错则为-1

    

 

 

 

 

 

 

 

 

key为共享存储的外部键,通过ftok获得。

size是该共享存储段的长度。如果正在创建一个新段,则必须指定其size。如果正在引用一个现存的段,则将size指定为0。当创建一新段时,段内的内容初始化为0

shmflg由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。当需要创建新的共享内存段时需要与IPC_CREAT标志按位或。设置IPC_CREAT标志并传递已存在的共享内存段不会产生错误。如果想创建一个读一无二的共享内存区可以与IPC_CREAT|IPC_EXCL按位或,这样如果系统以存在这个共享内存区,shmget函数就会报错。

下面是一个创建共享内存区的例子;

/*shmget.c创建共享内存区*/

#include <stdio.h>

#include <sys/shm.h>

#include <sys/ipc.h>

 

int main(int argc,char **argv)

{

int c,id,oflag;

char *ptr;

size_t length;

oflag=0644|IPC_CREAT;

 

if(argc!=3)

{

    printf(“usage:shmget <pathname> <length>\n”);

    exit(1);

}

 

length=atoi(argv[2]);

id=shmget(ftok(argv[1],0),length,oflag);

printf(“shm_id: %d\n”,id);

exit(0);

}

运行结果为:

#cc –o shmget shmget.c

#./shmget test 100

shm_id262147

       System V共享内存区至少具有随内核持续性,因此程序结束该共享内存区还存在。

 

在共享内存段刚被创建的时候,任何进程还都不能访问它。为了建立这个共享内存段的访问渠道,必须由我们来把它连接到某个进程的地址空间。这项工作是由shmat函数完成的。

2

名称:

shmat

功能:

将共享内存段连接到他的地址空间

头文件:

#include <sys/ipc.h>

#inlcude <sys/shm.h>

函数原形:

void *shmat(int shm_id,void *shm_addr,int shmflg);

参数:

shm_id 标识码

shm_addr 连接到的地址

shmflg  标志位

返回值:

若成功则为指向共享存储的指针,若出错则为-1

     

 

 

 

 

 

 

 

 

 

 

       shm_id是shnget返回的共享内存标识码。

       shm_addr是把共享内存连接到当前进程去的时候准备放置它的那个地址。这通常是一个空指针,表示把选择共享内存出现处的地址这项工作交给系统去完成。

       shmflg是一组按位或的标志。它的两个可能值是SHM_RND(这个标志与shm_addr一起控制着共享连接的地址)SHM_RDONLY(它使连接的共享内存成为一个只读区间)。很少有需要控制共享内存连接的地址的情况,一般都是由系统替你挑选一个地址,否则就会使你的软件对硬件的依赖性过高。

       shmat的返回值是该段所连接的实际地址,如果出错则返回-1。如果shmat成功执行,那么内核将该共享存储段shmid_ds结构的shm_nattch计算器加1

       缺省情况下,只要调用进程具有某个共享内存区的读写权限,它附接该内存区后就能够同时读写该内存区。只有flag参数指定SHM_RDONLY值时,它以只读方式访问。

 

当一个进程完成某个共享内存区的使用时,它可调用shmdt函数脱离与这个共享内存区的联系。

3

名称:

shmdt

功能:

脱接共享存储段

头文件:

#include <sys/ipc.h>

#inlcude <sys/shm.h>

函数原形:

int shmdt(void *shmaddr);

参数:

shmaddr

返回值:

若成功则为0,若出错则为-1

      

 

 

 

 

 

 

当一个进程终止时,它的所有当前附接着的共享内存区都自动断接掉。注意本函数调用并不是从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程调用shmctl特地删除它。 
addr参数是以前调用shmat时的返回值。如果成功,shmdt将使相关shmid_ds结构中的shm_nattch计数其减1。

4、

名称::

shmctl

功能:

对共享存储段执行多种操作

头文件:

#include <sys/ipc.h>

#inlcude <sys/shm.h>

函数原形:

int shmctl(int shm_id,int command,struct shmid_ds *buf);

参数:

shm_id 共享内存标识码

command 功能选择

buf 指向shmid_ds结构的指针

返回值:

若成功则为0,若出错则为-1

     

 

 

 

 

 

 

       shmctl提供了对一个共享内存区的多种操作。 
       shmid为共享存储的ID,用于内部标识共享存储。cmd参数指定下列命令中的一种,使其在shmid指定的段上执行。

       IPC_STAT      取此段的shmid_ds结构,并将它存放在由buf指向的结果中。

      IPC_SET   按buf指向结构中的值设置与此段相关结构中的下列三个字段:shm_perm.uid , shm_perm.gid 和shm_perm.mode.此命令只对有效用户ID等于shm_perm.cuid或shm_perm.uid的进程和具有超级用户特权的用户有效。

       IPC_RMID     从系统中删除该共享存储段。因为每个共享存储段有一个连接计数,所以除非使用该段的最后一个进程终止或与该段脱节,否则不会世界上删除该段。

       SHM_LOCK   将共享存储段锁定在内存中。此命令只能用超级用户执行。

       SGM_UNLOCK 解锁共享存储段。此命令只能用超级用户执行。

       buf是一个指针,它指向一个保存着共享内存的模式状态和访问权限的数据结构。

  

当建立共享内存区并把连接到该共享内存区,我就可以通过往该区域写或读来进程进程间通信。我们可以把该区域当成用malloc申请的内存一样操作。

 

/*shmwrite.c向共享内存区写数据*/#include <stdio.h>#include <sys/ipc.h>#include <sys/shm.h>main(int argc,char **argv){int i,id;unsigned char *ptr;struct shmid_ds buff;if(argc!=2){    printf("usage:shmread <pathname>\n");    exit(1);}/* 创建共享内存区 */if((id=shmget(ftok(argv[1],0),0,0))<0){    perror("shmget fail");    exit(1);}/* 将共享内存区连接到自己的地址空间 */if((ptr=shmat(id,NULL,0))<0) {    perror("shmat fail");}else {/* 使用共享内存区 */strcpy(ptr,"hello linux");}exit(0);}/*shmread.h从共享内存区读出数据*/#include <stdio.h>#include <sys/ipc.h>#include <sys/shm.h>main(int argc,char **argv){int i,id;unsigned char *ptr;struct shmid_ds buff;if(argc!=2){    printf("usage:shmread <pathname>\n");    exit(1);}if((id=shmget(ftok(argv[1],0),0,0))<0){    perror("shmget fail");    exit(1);}ptr=shmat(id,NULL,0);printf("%s\n",ptr);/*删除内存区*/if((shmctl(id,IPC_RMID,NULL))<0)    perror("shmctl fail");printf("shm_id: %d be removed \n",id);exit(0);}

 

记录锁

 

1.记录锁的功能

       当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。我们不应该从字面上去理解记录锁,实际上它应该叫“区域锁”,因为它锁定的只是文件的一个(也可能是整个文件)。这个区域用来存放多用户的共享区。

2.记录锁的分类

       记录锁分为共享读锁和独占写锁,前者也叫做共享锁后者也叫做排他锁。

3.加锁规则

      如果一个进程对共享区加了共享读锁,其他进程只能加共享读锁。如果一个进程加了独占写锁,其他进程就不能加任何锁。

4.死锁

       如果两个相互等待对方持有并且不释放(已被锁定)的资源是时,则这两个进程就处于死锁状态。如果一个进程已经控制了文件的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,则它就会睡眠,在这种情况下,有发生死锁的可能性。

5.锁的隐含继承和释放

1)锁与进程和文件两方面有关系,它和前者关系是:当一个进程结束后,他对文件加的锁也就消失了。它和后者的关系是:当进程close文件描述符,切断文件和进程的联系进程所创建的锁也会消失。

2)由fork产生的子进程不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程创建的锁而言,子进程被视为另一个进程,不会拥有该锁。

3)在执行exec后,新进程可以继承原执行的锁。因为执行exec前后还是一个进程。我们只是改变进程执行的程序,并没有创建新的进程。

6.要注意的问题

    记录锁只是提供竞争进入某段代码区的功能,不会导致对文件操作失败。也就是说,我们对文件进行加锁后,我们还是可以对文件进行操作。

 

1

名称:

fcntl

功能:

对文加解锁。

头文件:

#include <pthread.h>

函数原形:

int fcntl(int filedes,int cmd,…/*struct flock *flockptr */)

参数:

filedes   文件描述符

cmd     测试锁或加锁

flockptr  指向flock结构的指针

返回值:

若成功返回0,若失败返回错误编号。

     

 

 

 

 

 

 

 

 

 

对于记录锁,cmdF_GETLK,F_SETLKWF_SETLKW.

       F_GETLK判断由flockptr所描述的锁是否会被另外一把锁排斥。如果存在一把锁,他阻止创建由flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在这种情况除了将l_type设置为F_UNLCK之外,flockptr所描述的其他信息都不变。

F_SETLKF_SETLKW企图建立一把锁。F_SETLKF_SETLKW的区别是F_SETLKWF_SETLK的阻塞版本。如果存在其他锁,调用的进程就被阻塞直道捕捉到信号。

       第三个参数是一个指向flock结构的指针:

struct flock{

       short l_type; /*F_RDLCK,F_WRLCK,F_UNLCK*/

       off_t l_start; /*加锁的地址*/

       shout l_whence; /*加锁的偏移地址*/

       off_t l_len; /*加锁区域的长度*/

       pid_t l_pid; /*持有锁的进程ID*/

};

flock结构说明:

所希望的锁类型:F_RDLCK(共享读锁)F_WRLCK(独占性写锁)F_UNLCK(解锁一个区域),这是由 l_type决定的。

要加锁或解锁区域的起始字节偏移量,这是由l_stattl_whence两者决定。

区域的字节长度,由l_len表示。

具有能阻塞当前进程的锁,其持有的ID存放在l_pid中。

       如若l_len0,则表示锁的区域从其起点(由l_startl_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于的范围。

       如果想锁住整个文件,通常的方法是将l_start说明为0l_whence说明为SEEK_SET1_len说明为0

       还要注意的是,对文件加共享读锁时文件应以只读的方式打开,对文件加独占写锁时文件应以只读的方式打开。

下面是给一个文件加锁和测试锁的程序。

/*12_1.c加锁程序*/

#include <stdlib.h>

#include <fcntl.h>

 

int main(int argc,char *argv[])

{

int fd;

struct flock lock;

 

if((fd=open(argv[1],O_WRONLY))<0)

    perror(argv[1]);

lock.l_type=F_WRLCK; /*设置flock结构*/

lock.l_start=0;

lock.l_whence=SEEK_SET;

lock.l_len=0;

 

if(fcntl(fd,F_SETLK,&lock)<0) /*加锁整个文件*/

{

    perror(argv[1]);

    exit(1);

}

sleep(10);

close(fd);

exit(0);

}

 

/*12_2.c测试锁程序*/

#include <stdlib.h>

#include <fcntl.h>

 

int main(int argc,char *argv[])

{

int fd;

struct flock lock;

char buf[]=”123456”;

 

if((fd=open(argv[1],O_WRONLY))<0)

    perror(argv[1]);

lock.l_type=F_WRLCK; /*设置flock结构*/

lock.l_start=0;

lock.l_whence=SEEK_SET;

lock.l_len=0;

 

if(fcntl(fd,F_SETLK,&lock)<0)/*测试共享资源是否加锁*/

{

    perror(argv[1]);

    exit(1);

}

if(lock.l_type==F_UNLCK)

    printf(“Is not clocked!\n”);

else {

    printf(“Is clocked!\n”);

    exit(1);

}

if(write(fd,buf,sizeof(buf))<0)

    perror(“wrire error”);

close(fd);

exit(0);

}

 

先在后台运行加锁程序

#./12_1 12_1.c&

然后在十秒之内运行测试锁的程序

#./12_2 12_1.c

在屏幕上会打印Is clockd!

如果等待1012_1.c运行完

再次运行测试锁的程序则会打印Is not clocked!

 

 Posix条件变量

 

一、什么是条件变量

       与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

       条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

       使用条件变量之前要先进行初始化。可以在单个语句中生成和初始化一个条件变量如:

pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER;(用于进程间线程的通信)。

也可以利用函数pthread_cond_init动态初始化。

 

二、条件变量函数

1

名称:

pthread_cond_init

目标:

条件变量初始化

头文件:

#include < pthread.h>

函数原形:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

参数:

cptr  条件变量

attr  条件变量属性

返回值:

成功返回0,出错返回错误编号。

     

 

 

 

 

 

 

 

pthread_cond_init函数可以用来初始化一个条件变量。他使用变量attr所指定的属性来初始化一个条件变量,如果参数attr为空,那么它将使用缺省的属性来设置所指定的条件变量。

 

2.

名称:

pthread_cond_destroy

目标:

条件变量摧毁

头文件:

#include < pthread.h>

函数原形:

int pthread_cond_destroy(pthread_cond_t *cond);

参数:

cptr  条件变量

返回值:

成功返回0,出错返回错误编号。

     

 

 

 pthread_cond_destroy函数可以用来摧毁所指定的条件变量,同时将会释放所给它分配的资源。调用该函数的进程也并不要求等待在参数所指定的条件变量上。

 

3.

名称:

pthread_cond_wait/pthread_cond_timedwait

目标:

条件变量等待

头文件:

#include < pthread.h>

函数原形:

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t mytex,const struct timespec *abstime);

参数:

cond 条件变量

mutex 互斥锁

返回值:

成功返回0,出错返回错误编号。

      

 

 

 

 

 

 

 

第一个参数*cond是指向一个条件变量的指针。第二个参数*mutex则是对相关的互斥锁的指针。函数pthread_cond_timedwait函数类型与函数pthread_cond_wait,区别在于,如果达到或是超过所引用的参数*abstime,它将结束并返回错误ETIME.pthread_cond_timedwait函数的参数*abstime指向一个timespec结构。该结构如下:

typedef struct timespec{

       time_t tv_sec;

       long tv_nsex;

}timespec_t;

 

3.

名称:

pthread_cond_signal/pthread_cond_broadcast

目标:

条件变量通知

头文件:

#include < pthread.h>

函数原形:

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

参数:

cond 条件变量

返回值:

成功返回0,出错返回错误编号。

     

 

 

 

 

 

 

参数*cond是对类型为pthread_cond_t 的一个条件变量的指针。当调用pthread_cond_signal时一个在相同条件变量上阻塞的线程将被解锁。如果同时有多个线程阻塞,则由调度策略确定接收通知的线程。如果调用pthread_cond_broadcast,则将通知阻塞在这个条件变量上的所有线程。一旦被唤醒,线程仍然会要求互斥锁。如果当前没有线程等待通知,则上面两种调用实际上成为一个空操作。如果参数*cond指向非法地址,则返回值EINVAL

 

 

三、条件变量属性

       使用条件变量之前要先进行初始化。可以像我们前面那样可静态初始化pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER;也可以利用函数pthread_cond_init动态初始化。条件变量属性类型为pthread_condattr_t,它们由以下函数初始化或摧毁。

 

5

名称:

pthread_condattr_init/pthread_condattr_destroy

功能:

初始化/回收pthread_condattr_t结构

头文件:

#include <pthread.h>

函数原形:

int pthread_condattr_init(pthread_condattr_t *attr);

int pthread_condattr_destroy(pthread_condattr_t *attr);

参数:

 

返回值:

若成功返回0,若失败返回错误编号。

 

     

 

 

 

 

 

 

一旦某个条件变量对象被初始化了,我们就可以利用下面函数来查看或修改特定属性了。

6.

名称:

pthread_condattr_getpshared/pthread_condattr_setpshared

功能:

查看或修改条件变量属性

头文件:

#include <pthread.h>

函数原形:

int pthread_condattr_init(const pthread_condattr_t *restrict attr);

int pthread_condattr_destroy(pthread_rwlockattr_t *attr,int pshared);

参数:

 

返回值:

若成功返回0,若失败返回错误编号。

     

 

 

 

 

 

 

pthread_condattr_getpshared函数在由valptr指向的整数中返回这个属性的当前值,pthread_condattr_setpshared则根据value的值设置这个属性的当前值。value的值可以是PTHREAD_PROCESS_PRIVATEPTHREAD_PROCESS_SHARED(进程间共享).

 

四、条件变量与互斥锁、信号量的区别

到这里,我们把posix的互斥锁、信号量、条件变量都接受完了,下面我们来比较一下他们。

       1.互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。

       2.互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)。

       3.由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。

       4.互斥锁是为了上锁而优化的,条件变量是为了等待而优化的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。



0 0
原创粉丝点击