分类: LINUX
2009-04-10 18:27:04
1.4 一个例子
该例子实现子进程通过管道从父进程接受命令并执行相应操作。程序首先创建一个管道,然后fork一个子进程。父进程从标准输入中读取用户输入的命令,然后把命令写入管道;子进程从管道中读出命令并执行相应操作。注:例子只实现了ls功能,命令输入为ls.下面是程序编译运行结果:
点击此处下载
2、命名管道(FIFO)
FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。
2.1、命名管道的创建
int mkfifo(const char * pathname, mode_t mode)
该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。
2.2、FIFO的打开规则:
如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
2.3、FIFO的读写规则
从FIFO中读取数据:
如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
向FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
对于设置了阻塞标志的写操作:
(1) 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
(2) 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
对于没有设置阻塞标志的写操作:
(1) 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
(2) 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;
2.4 一个例子
服务进程(即写入端进程),首先创建一个命名管道,然后从标准输入中读取信息并把信息写入命名管道。客户进程(即读出端进程)打开命名管道,从命名管道中读取数据并显示在标准输出上。把服务进程(即写入端进程)编译成wfifo,客户进程(即读出端进程)编译成rfifo.然后首先运行客户进程,再运行服务进程。在服务进程中输入信息在客户进程中就可以显示出来。程序编译运行结果如下:
点击此处下载
3.XSI IPC
消息队列、信号量和共享存储称作XSI IPC,它们之间有很多相似之处。每个内核中的XSI IPC结构都用一个非负整数的标识符(des)加以引用。当一个XSI IPC结构被创建,以后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正值,然后又回转到0. 标识符是IPC对象的内部名, 而它的外部名则是key(键), 它的基本类型是key_t, 在头文件
有多种方法使客户进程和服务进程在同一XSI IPC结构上会合:
(1) 服务器进程可以指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(例如一个文件)。注意,为了访问一个现存队列,决不能指定IPC_PRIVATE作为键。因为这是一个特殊的键值,它总是用于创建一个新队列。
(2) 在一个公用头文件中定义一个客户进程和服务器进程都认可的键。
(3) 客户进程和服务器进程认同一个路径名和项目ID,接着调用函数ftok将这两个值变换为一个键。
注:可以使用ipcs命令查看系统中XSI IPC的信息。
3.1 消息队列
消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的。每个消息队列都有一个队列头,用结构struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。读者可以访问这些信息,也可以设置其中的某些信息。
struct mymesg{
long mtype;
char mtext[len];
}
mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容。因此,对于发送消息来说,首先预置一个mymesg缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个mymesg缓冲区,然后把消息读入该缓冲区即可。
3.1.1 相关API
int msgget(key_t key, int msgflg)
参数key是一个键值,由ftok获得;msgflg参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。在以下两种情况下,该调用将创建一个新的消息队列:
如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;
key参数为IPC_PRIVATE;
参数msgflg可以为以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者相或结果。
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag)
向msgid代表的消息队列发送一个消息,即将发送的消息存储在ptr指向的mymesg结构中,消息的大小由nbytes指定。对发送消息来说,有意义的flag标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag)
该系统调用从msgid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。msqid为消息队列描述字;消息返回后存储在ptr指向的地址,nbytes说明数据缓冲区的长度。type为请求读取的消息类型;读消息标志flag可以为以下几个常值的或:
IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG
IPC_EXCEPT 与type>0配合使用,返回队列中第一个类型不为type的消息
IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的nbytes字节,则把该消息截断,截断部分将丢失。 否则出错返回E2BIG。
参数type使我们可以指定想要哪一种消息:
type == 0 返回队列中的第一个消息
type > 0 返回队列中消息类型为type的第一个消息
type < 0 返回队列中消息类型值小于或等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
该系统调用对由msqid标识的消息队列执行cmd操作,共有三种cmd操作:
IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的结构中;
IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、 msg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。
IPC_RMID:删除msqid标识的消息队列,这种删除立即生效。
消息队列与管道以及命名管道相比,具有更大的灵活性,首先,它提供有格式字节流,有利于减少开发人员的工作量;其次,消息具有类型,在实际应用中,可作为优先级使用。这两点是管道以及命名管道所不能比的。同样,消息队列可以在几个进程间复用,而不管这几个进程是否具有亲缘关系,这一点与命名名管道很相似;但消息队列是随内核持续的,与命名管道(随进程持续)相比,生命力更强,应用空间更大。
3.1.2 一个例子
send_msg为服务进程(即发送消息的进程),recv_msg为客户进程(即接收消息的进程)。服务进程创建消息队列并写消息进队列,客户进程从队列中读消息,如果输入quit则退出程序。创建一个消息队列时,指定IPC_CREAT和IPC_EXCL位,如果返回值为-1就判断错误号是否为EEXIST,如果是的话就只指定IPC_CREAT位。这样不管队列存不存在都程序都可以执行。下面代码实现上述功能:
key = ftok(MSGPATH,'a');
msgid = msgget(key,IPC_CREAT | IPC_EXCL | 0666);
if(msgid==-1)
{
if(errno==EEXIST)
msgid = msgget(key,IPC_CREAT | 0666);
else
{
printf('msgqueue create error
');
return -1;
}
}
队列创建后可以用ipcs命令查看:
服务进程(即发送消息的进程)编译成smsg,客户进程(即接收消息的进程)编译成rmsg,运行结果如下:
点击此处下载 (文件大小:1K) (文件大小:1K)
3.2 共享存储
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,
同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之
亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷
贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数
据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总
是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,
这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因
此,采用共享内存的通信方式效率是非常高的。
内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。本文主要讨论
mmap系统调用和POSIX共享内存的原理及应用。
3.2.1内存映射文件及其相关系统调用
内存映射文件不仅仅用于IPC,在其他进程中它也有很大作用。利用内存映射来处理IPC的好处是在整个过程中你不需要处理句柄:只要打开文件并把它映射在合适的位置就行了。你可以在两个不相关的进程间使用内存映射文件。使用内存映射的缺点是速度不如共享内存快。如果凑巧文件很大,所需要的虚拟内存就会很大,这样会造成整体性能下降。
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
void* mmap (void * addr , size_t len , int prot , int flags , int fd , off_t offset)
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags
参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于
具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算
起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可
写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED ,
MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使
用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指
定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可
直接操作起始地址为该值的有效地址。
系统调用mmap()用于共享内存有两种方式:一种为使用普通文件提供的内存映射,适用于任何进程之间。此时,需要打开或创建一个文件,然后再调用mmap()。另一种方式为使用特殊文件提供匿名内存映射,适用于具有亲缘关系的进程间通信。此时,在父进程中先调用mmap(),然后调用fork()。在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,父子进程就可以通过映射区域进行通信了。
int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
3.2.2 POSIX共享内存
POSIX共享内存共享内存指的是把所有共享数据放在共享内存区域(IPC shared memory region),任
何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。系
统调用mmap()通过映射一个普通文件实现共享内存,POSIX则是通过映射特殊文件系统shm中的文件实现进程间的
共享内存通信。POSIX共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证
shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构注同时,还将在特殊文件系统
shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何
一个进程(任何进程都可以访问该共享内存区)。
在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。
int shmget(key_t key, size_t size, int flag)
shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。参数size是该共享存储段的长度(单位:字节),实现通常将其取为系统页长的整数倍。若应用指定的size值不是系统页长的整数倍,则最后一页余下部分是不可用的。如果正在引用一个现存的段,则将size指定为0。当创建一新段时,段内的内容初始化为0。
void *shmat(int shmid, const void *addr, int flag)
shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。一般指定addr为0,由内核选择地址,这样程序的移植性较好。
int shmdt(void *addr)
shmdt()调用用来解除进程对共享内存区域的映射。
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
shmctl实现对共享内存区域的控制操作。cmd参数指定下列5种命令中一种,使其在shmid指定的段上执行:IPC_STAT、IPC_SET、IPC_RMID、SHM_LOCK、SHM_UNLOCK。
POSIX共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内
存通信可以指定何时将数据写入磁盘文件中。POSIX共享内存是随内核持续的,即使所有访问共享内存的进程都已经
正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操
作都将一直保留。通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。
而通过POSIX共享内存实现通信的进程则不然。
3.3 信号量
信号量与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标
志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用
于进程同步。信号量有以下两种类型:
二元信号量:最简单的信号量形式,信号量的值只能取0或1,类似于互斥锁。注:二元信号量能够实现互
斥锁的功能,但两者的关注内容不同。信号量强调共享资源,只要共享资源可用,其他进程同样可以修改信号量的
值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
计算信号量:信号量的值可以取任意非负值(当然受内核本身的约束)。
3.3.1 系统调用API
int semget(key_t key, int nsems, int flag)
参数key是一个键值,由ftok获得,唯一标识一个信号量集,用法与msgget()中的key相同;参数nsems指定
打开或者新创建的信号量集中将包含信号量的数目,如果是引用一个现存的集合,则将nsems指定为0;flag参数是
一些标志位。参数key和flag的取值,以及何时打开已有信号量集或者创建一个新的信号量集与msgget()中的对应部
分相同。
注:如果key所代表的信号量已经存在,且semget指定了IPC_CREAT|IPC_EXCL标志,那么即使参数nsems与原来信号量的数目不等,返回的也是EEXIST错误;如果semget只指定了IPC_CREAT标志,那么参数nsems必须与原来的值一致。
int semop(int semid, struct sembuf *sops, size_t nops)
semid是信号量集ID,sops指向数组的每一个sembuf结构都刻画一个在特定信号量上的操作。nops为sops
指向数组的大小。
sembuf结构如下:
struct sembuf {
unsigned short sem_num; /* semaphore index in array */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
sem_num对应信号集中的信号量,0对应第一个信号量。sem_flg可取IPC_NOWAIT以及SEM_UNDO两个标志。如
果设置了SEM_UNDO标志,那么在进程结束时,相应的操作将被取消,这是比较重要的一个标志位。如果设置了该
标志位,那么在进程没有释放共享资源就退出时,内核将代为释放。如果为一个信号量设置了该标志,内核都要分配
一个sem_undo结构来记录它,为的是确保以后资源能够安全释放。事实上,如果进程退出了,那么它所占用就释放
了,但信号量值却没有改变,此时,信号量值反映的已经不是资源占有的实际情况,在这种情况下,问题的解决就靠
内核来完成。这有点像僵尸进程,进程虽然退出了,资源也都释放了,但内核进程表中仍然有它的记录,此时就需要
父进程调用waitpid来解决问题了。
这里需要强调的是semop同时操作多个信号量,在实际应用中,对应多种资源的申请或释放。semop保证操作的原
子性,这一点尤为重要。尤其对于多种资源的申请来说,要么一次性获得所有资源,要么放弃申请,要么在不占有任
何资源情况下继续等待,这样,一方面避免了资源的浪费;另一方面,避免了进程之间由于申请共享资源造成死锁。
也许从实际含义上更好理解这些操作:信号量的当前值记录相应资源目前可用数目;sem_op>0对应相应
进程要释放sem_op数目的共享资源;sem_op=0可以用于对共享资源是否已用完的测试;sem_op<0相当于进程要
申请-sem_op个共享资源。
int semctl(int semid,int semnum,int cmd,union semun arg)
该系统调用实现对信号量的各种控制操作,参数semid指定信号量集,参数cmd指定具体的操作类型;参数
semnum指定对哪个信号量操作,只对几个特殊的cmd操作有意义;arg用于设置或返回信号量信息,该参数是可选
的,其类型是semun,它是多个特定命令参数的联合,而非指向联合的指针。
Semun结构如下:
union semun {
int val; /*for SETVAL*/
struct senid_ds *buf; /*for IPC_STAT and IPC_SET*/
unsigned short *array; /*for GETALL and SETALL*/
};
注意:此联合在有些系统中需要自己定义,如果没有定义则会出现storage size of `sem_union' isn't known的错误。
3.4 一个例子
这个例子包含了共享内存和信号量,因为通常,信号量被用来实现对共享内存访问的同步。服务进程创建共享内存然后把数据写入,接着用信号量通知客户进程进行数据读取。下面是程序运行结果:
点击此处下载 (文件大小:1K) (文件大小:1K)