http://www.cnblogs.com/dubingsky/archive/2009/06/18/1505722.html
主要手段
1.管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信; 有名管道克服了管道没有名字的限制,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2.信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身。
3.消息队列:消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4.共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
5.信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
6.套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。
一.管道 (Windows管道)
管道是Linux支持的最初Unix IPC形式之一,特点:
1.管道是半双工的(数据只能向一个方向流动);需要双方通信时,需要建立起两个管道;
2.只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
管道是由调用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: */
47:
下面网上一篇文章介绍fork()
创建进程一个进程的生命起点是fork(),由其父进程调用以产生该进程。fork()是UNIX系统上唯一一个能够创建新进程的调用,当然,除了init进程,这个进程是由内核在系统boot时创建的,其PID是0。系统启动完成后,init进程将推出,取而代之的是PID为1的swap进程(有时候又叫做idle进程)。 在执行fork()时,kernel将会做一些列的操作:1、在进程表中为新进程申请一个表项;2、为新进程分配一个唯一的PID;3、将父进程的上下文及地址空间做一个逻辑拷贝——一般来说,只需要拷贝数据段和堆栈段等,文本段无需拷贝,因为父子进程共享该段。若子进程立即调用exec()家族的函数,则更不需要拷贝文本段,数据段和堆栈段也无需拷贝,因为将会有一个全新的image加载到子进程并运行;4、因为父子进程共享文件,所以增加文件和inode的引用计数;5、将子进程的状态设置为Read to Run;6、返回两次:将子进程PID返回给父进程(可用于跟踪子进程状态),将0返回给子进程; 关于fork()的实现,请求分页和交换系统有所不同,本文将讨论交换系统上的实现,并假定有足够的内存来创建子进程。以后讨论分页系统的实现。下面是伪代码:PID fork(){ Assert(EnoughMemory && EnoughResources); PID aPid = GetFreeProcTableEntry(); // 同时得到进程表项。 Assert(NumOfProcs(GetCurrentUser()) < MAX_PROC_NUM_PER_USER); // 保证一个用户不会创建太多进程。 procTable[aPid]->State = CREATED; CopyData(procTable[aPid], procTable[getppid()]); // 从父进程的表项中拷贝数据,如有效uid,当前目录等等。 增加当前目录及改变的根的inode引用计数; 增加文件表中打开文件的引用计数; 拷贝一份父进程的上下文(包括u area,文本段、堆栈段、数据段); 将一个假系统级上下文层压到子进程的系统上下文栈,该上下文层包括能够让子进程识别自己的一些信息,并且当子进程获得处理器时从此处开始执行; // 就像进程被调度时保存的上下文。 if(IsParent()) // 一旦被调度,将从假系统级上下文层开始执行,父子进程将共享下面的代码。 { procTable[aPid]->State = READY_TO_RUN; // 此时子进程才可能被调度。 return aPid; } else { 初始化u area的与分时相关的一些字段; return 0; }}fork()的实现是相当复杂的,但是,抓住三点就好理解了:上下文、调度、共享资源。在将子进程放入调度队列之前,先设置子进程的上下文信息(包含那个假系统级上下文层)。当子进程被调度,它将执行从“if (IsParent())”开始的那段代码。同样,父进程在设置子进程的上下文信息后也执行这段代码,看起来好像父子进程都是在该点“被调度”,从运行态转为其他状态。 在交换系统上,创建进程时需要两份空间来存放子进程:一份在内存,一份在磁盘——因为子进程的状态为READ_TO_RUN,如果此时系统没用可用内存,而它又处于低优先级,它就有可能被交换到磁盘上。另外需要注意的是,在获取空闲的进程表项时需要有一定的锁机制来避免竞态条件。 获取了进程表项之后,子进程便处于“被创建”(或“正被创建”)状态。此时,内核将会让这个新生儿“继承”其父进程的进程表项,并将父进程的pid设置到子进程,以便子进程识别父进程。同时,子进程将被放到进程的树结构中——同时还会设置其优先级、初始化调度参数等等。 之后便是调整一些引用计数。关于打开的文件的引用计数,大家可以用经典的“协同进程”方法做实验。 还记得u area吗?只有通过u area,内核才能识别一个运行中的进程。它也是进程上下文中静态系统级上下文的一员。然而,在fork()时,除了其中进程表项的指针,父子进程的u area完全一样。当然,在fork()执行完之后,这两个u area就可能不一样了。 好了,进程所需的所有的静态上下文都已经创建好了,内核需要做的就是为子进程populate一个假系统上下文层。大家要注意了!内核将父进程的第一层上下文拷贝给子进程,该层包括用户保存的寄存器上下文和调用fork()的内核栈楨,然后创建一个假的系统上下文层,该层保存了上一层以及其他的寄存器信息,设置正确的PC(“if (IsParent())”),以保证子进程能够被正确的“恢复执行”——虽然子进程从来没有执行过。
管道两端可分别用描述字fd[0]以及fd[1]来描述,一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。一般文件的I/O函数都可以用于管道,如close、read、write等等。
从管道中读取数据:如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0; 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,否则返回你请求的字节数。注意的是,读出的字节数是你Write设置的大小。
向管道中写入数据:向管道中写入数据时,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。
只支持单向数据流;
只能用于具有亲缘关系的进程之间;
没有名字;
缓冲区有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;
有名管道(named pipe或FIFO)不同于管道之处在于它提供一个路径名与之关联,以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,如close、read、write等等。
下面是不同祖先通过管道通信的例子。
/* 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)); }}本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lcrystal623/archive/2007/03/16/1531201.aspx
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位。
1)pathname一定要在系统中存在
2)pathname一定是使用进程能够访问的
3)proj_id是一个1-255之间的一个整数值,典型的值是一个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结构时,对所有字段都赋初值。可以调用msgctl、semctl或shmctl修改uid,gid和mode字段。
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标志位;
2.key参数为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_STAT、IPC_SET 、IPC_RMID。
IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid_da结构中;
IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid_ds结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_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.不满足上述两个条件,即消息队列中有容纳该消息的空间;
2.msqid代表的消息队列被删除;
3.调用msgsnd()的进程被信号中断;
当msgsnd成功返回,与消息队列相关的msqid_ds结构得到更新,以标明发出该调用的进程ID(msg_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指定msgbuf的mtext成员的长度(即消息内容的长度),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结构得到更新,以标明发出该调用的进程ID(msg_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
一、概念
信号灯(也叫信号量)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。信号灯是进程/线程同步的一种方式,有时候我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关;有时候需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号灯。信号灯开关是二进制信号灯的一种逻辑扩展,两者实际调用的函数都是一样的。
信号灯分为以下三种。
一、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结构的uid和cuid成员被置为调用进程的有效用户ID,gid和cgid成员被置为调用进程的有效组ID。
sem_otime被置为0,sem_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_CREAT和IPC_EXCL标志的联合使用确保自己将创建出一个新的独一无二的信号量来,如果该信号量已经存在,就会返回一个错误。
当实际操作为创建一个信号灯时,相应的semid_ds结构的以下成员将被初始化:
与该集合中每个信号灯关联的各个sem结构并不初始化。这些结构是在以SET_VAL或SETALL命令调用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);}
一、什么是共享内存区
共享内存区是最快的可用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_id:262147
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,若失败返回错误编号。 |
对于记录锁,cmd是F_GETLK,F_SETLKW或F_SETLKW.。
F_GETLK判断由flockptr所描述的锁是否会被另外一把锁排斥。如果存在一把锁,他阻止创建由flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在这种情况除了将l_type设置为F_UNLCK之外,flockptr所描述的其他信息都不变。
F_SETLK和F_SETLKW企图建立一把锁。F_SETLK和F_SETLKW的区别是F_SETLKW是F_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_statt和l_whence两者决定。
区域的字节长度,由l_len表示。
具有能阻塞当前进程的锁,其持有的ID存放在l_pid中。
如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于的范围。
如果想锁住整个文件,通常的方法是将l_start说明为0,l_whence说明为SEEK_SET,1_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!
如果等待10秒12_1.c运行完
再次运行测试锁的程序则会打印Is not clocked!
一、什么是条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
使用条件变量之前要先进行初始化。可以在单个语句中生成和初始化一个条件变量如:
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_PRIVATE或PTHREAD_PROCESS_SHARED(进程间共享).
四、条件变量与互斥锁、信号量的区别
到这里,我们把posix的互斥锁、信号量、条件变量都接受完了,下面我们来比较一下他们。
1.互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。
2.互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)。
3.由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
4.互斥锁是为了上锁而优化的,条件变量是为了等待而优化的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。