10、客户服务特性
我们来详细看一下客户和服务进程的特性,它们被所使用的各种IPC所影响。
模型1:客户端是父进程服务端是子进程
最简单的客户服务进程关系就是客户进程调用fork执行服务进程。可以在fork之前建立两个半双工管道,以便可以在两个方向传输数据(类似下图)。服务程序被设置成具有set-user-id权限,这样它会有特殊的权限。我们也可以查看客户进程的real user id来判断客户进程的real user id(执行exec并不会改变real user id)。
+parent-------------+ +child(coprocesses)--+
| fd1[1] |<----pipe1------| stdin |
| fd2[0] |-----pipe2----->| stdout |
+-------------------+ +--------------------+
通过这个结构,我们建立了一个打开的服务者进程。(我们以后会给出这个客户服务的实现) 服务进程为客户进程打开文件 而不是客户进程调用open函数。通过这个方式, 可以添加比user/group/other更丰富的额外权限检查 (见后面译者注1)。我们假设服务进程是set-user-id程序,这样Server进程具备了额外的权限(可能就是root权限)。这个服务进程使用客户进程的real user id来确定是否让客户进程访问它所请求的文件。通过这个方式,我们可以建立一个服务进程,这个服务进程可以让特定的用户具有他们一般情况下没有的权限(见后面译者注2)。
在这个例子,因为服务进程是父进程的一个子进程,服务进程所能够做的就是将文件的内容传递回父进程(注意,是文件内容而非文件描述符)。虽然这个在普通文件的时候工作的很好,但是它不能用于特殊设备文件。例如,我们想让服务进程打开一个指定的文件,并且将文件描述符号传递回去。然而,一个父进程可以将一个打开的文件描述符号传递给子进程,但是一个子进程不能构将一个文件描述符号传递给父进程(除非使用一个特殊的编程技术,后面我们会说到)。
模型2:一个守护服务端进程,多个客户端进程
在下一个客户服务关系中,我们展示了下面的图中显示的服务类型。这个服务进程是一个守护进程,通过某种形式的IPC和所有的客户进程相连接。我们不能在这种客户服务模型中使用pipes(匿名管道)。这种模型需要某种有名的IPC,例如FIFOs(有名管道)或者消息队列。通过FIFOs,我们可以看到如果服务进程想要把数据发送回客户进程,那么需要为这个客户进程创建一个额外的管道。如果客户服务程序发送的数据只是从客户进程发送到服务进程中,那么一个单一的公共FIFO就足够了。(System V的行打印就使用这种形式的模型。客户程序是lp命令,服务进程是lpsched守护进程。因为数据只是从客户向服务进程发送,所以只使用一个单一的FIFO。)
+---------------+
| server |
/ +-------^-------+ \
write replies | write replies
/ read requests \
+--------------v-+ +-------+-------+ +-v---------------+
|client-FIFO | | well-knownFIFO| | client-FIFO |
+---\------------+ +--^----------^-+ +-------------/---+
\ / \ /
\ write requests write requests /
read replies / \ read replies
\ / \ /
+v-----------+ +----------v+
| client | ...... | client |
+------------+ +-----------+
通过消息队列,有多种方法:
-
服务进程和所有的客户进程可以使用一个共同的消息队列。通过消息的type字段来标识消息的接收者。例如,客户进程发送一个type类型为1的消息请求给服务进程,请求数据里面包含客户的进程ID。服务进程接收到请求之后,可以通过接收到的进程ID,把客户进程ID填入到消息type中,反馈消息给客户进程。这样,服务进程只接收type字段为1的消息(通过msgrcv的第4个参数),客户进程只接收type为它自己进程ID的消息。
-
另外,每个客户进程还可以使用它们自己单独的消息队列。在发送第一条请求之前,每个客户先使用IPC_PRIVATE创建好它自己的消息队列。而服务进程本身有一个被所有客户进程知道的公共的消息队列。客户进程将第一条请求发送给服务进程的公共队列中,请求中包含了客户进程自己创建的消息队列的队列ID,服务进程接收到客户的第一个请求之后,就可以根据请求中的队列ID,开始通过客户进程自己的消息队列(而不再是公共的消息队列了)和客户进程之间开始交互了。
使用这个方法的一个问题就是,每个客户进程的消息队列中只有一条消息(发给服务进程的请求或者从服务进程发送回来的反馈),这对系统资源是一个浪费,我们可以使用FIFO来替代;另外一个问题就是服务进程需要从多个消息队列中读取消息,而对于消息队列,却没有类似select或者poll的函数。
以上两种使用消息队列的方法,都可以用共享内存或者同步机制(记录锁或者信号量)来替代。
这个客户服务类型关系(服务进程和客户进程之间没有直系关系)的问题 出现在服务进程 精确辨别客户进程 的上面。除非服务进程执行一个普通的的操作,否则(执行特别操作时)服务进程就需要知道客户进程是谁。如果服务进程是一个set-user-ID程序,这一点是非常必要的。尽管这些IPC机制都通过内核,但是并没有一个可用的方法让内核可以分辨出消息的发送者。
通过消息队列,如果在客户和服务进程之间使用一个单一的队列(这样只有一个消息在一个时间中在队列中),那么消息队列的msg_lspid成员包含了其他进程的进程ID。但是当写服务进程的时候,我们需要知道客户进程的有效用户ID而不是进程ID。目前没有一个可以移植的方法可以通过一个指定的进程ID获得相应的有效用户ID(虽然内核将两个值都保存在了进程表中,但是除非我们搜索内核的内存,否则我们没有办法通过其中的一个来获取另外一个值)。
用来辨别客户进程身份的技术
我们将在后面( UNIX Domain Sockets
) 使用一个技术来让服务进程辨别客户进程。我们也可以使用FIFOs,消息队列,信号量,或者共享内存来实现这个技术(参见译者注3)。
下面的描述中,我们假设使用的是FIFOs,就像前面那个图形中的那样。 客户必须创建它自己的FIFO ,然后设置文件访问权限为只读只写。我们假设服务进程具有超级用户权限(否则它可能不会关心客户进程的真实身份,因为超级用户权限比较危险,用这样的权限为客户服务要精确辨别客户是谁以便是否提供服务),这样服务进程也可以对这个FIFO进行读写。当服务进程接收到客户发送到服务进程的公共FIFO上面的第一个请求的时候(请求必须包含客户的FIFO的标识),服务进程调用stat或者fstat对客户相关的FIFO进行操作。服务进程假设客户进程的有效用户ID就是管道的属主(就是stat结构的st_uid成员),服务进程只验证用户读和用户写权限是否激活。另外,服务进程应该也查看相应的FIFO的三个时间(stat结构变量的st_atime,st_mtime,和st_ctime成员)来验证它们是最近的。如果一个非法用户可以用别人的身份做为拥有者创建一个FIFO,并且设置了文件的读写权限,那么系统就是不安全的。
如果使用XSI的IPC实现这个技术,那么使用前面我们说过的和消息队列,信号量和共享内存相关的ipc_perm结构来辨别创建IPC消息通信结构的创建者(通过cuid和cgid成员)。和使用FIFOs类似,服务进程 需要客户进程创建IPC结构 ,并且设置访问权限为只读只写。和IPC结构相关的时间信息也应当被服务进程验证,以便可以保证是最新的(IPC结构除非被显示地删除,否则它们会一直存在)。
我们后面将会看到一个更好的方法来做这个验证,就是让内核提供客户进程的有效用户ID以及有效用户组ID。这个在文件描述符号在进程之间传递的时候,通过STREAMS子系统来做。
译者注
这里提到了两种客户端服务端模型,也描述了它们的优点与缺点。
-
前面第一个模型中提到,服务进程帮客户进程打开文件,能提供
更丰富的额外权限检查
。在Server中帮助客户端(父进程)打开文件,并且将文件传给客户端之前,Server可以做更多的权限判断(比如检查请求它打开文件的客户端是谁,应当怎样访问文件等等),这样比直接在客户端打开文件利用Linux本身机制检查文件权限,更为丰富强大。 -
前面第一个模型中提到
服务进程可以让特定的用户具有他们一般情况下没有的权限
。 比如,普通进程直接访问文件,一般只能具备文件权限位规定的访问权限;但是若这个进程通过exec一个有set-user-id的进程(我们叫做服务进程)帮它打开文件,并将文件描述符传回来,那么,这个进程便间接地拥有了更多的文件访问权限(因为具有set-user-id的服务进程打开文件的时候,拥有了这个文件的owner权限,传回这样的文件描述符,导致原来的进程拥有了比直接打开文件更多的权限)。 -
第二个模型中,服务端要想精确辨别客户进程身份的技术,主要是通过文件或IPC的权限属性来判断,而非PID,前提是需要客户端来创建通信的接收端。
原文参考
11、总结
我们详细讲解了内部进程通信技术:pipes,有名管道(FIFOs),和三种XSI的通用IPC技术(消息队列,信号量,和共享内存)。信号量实际上是一种同步机制,并不是真正的IPC机制,它一般用来同步共享资源的访问,例如共享内存段。通过pipes,我们看到了popen函数的实现,协作处理进程,以及使用标准输入输出库时候缓存时候遇到的问题。
在对比了消息队列和全双工管道,信号量和记录锁的时间之后,我们可以得到如下建议:应该学习管道(pipes)和有名管道(FIFOs),因为这两种基本的技术在许多的应用程序中工作的效率还是非常高的。避免在新的应用程序中使用消息队列和信号量,可以使用全双工管道和记录锁来替代,因为它们使用起来要简单得多。尽管使用mmap函数也能实现类似共享内存的功能,但是共享内存还是有它的作用的。
在后面,我们将会看到网络的IPC技术,这允许进程之间跨越不同的机器进行通信。
网友评论