UNIX系统中的进程间通信与网络编程
1. 进程间通信(IPC)
在UNIX系统中,进程间通信是一个重要的话题,下面将介绍几种常见的IPC机制。
1.1 消息队列
消息队列允许进程之间以消息的形式交换数据。服务器创建一个新的消息队列,任何人都可以对其进行读写操作。为了确保系统中没有其他进程使用相同的键值,我们使用
IPC_EXCL
。示例代码如下:
exit(0);
}
% msq-srvr &
% msq-clnt < /etc/motd
服务器创建消息队列后,会从队列中接收消息。类型为1的消息是数据,会被打印到标准输出。由于消息队列没有文件结束的概念,我们使用类型为2的消息来告知服务器没有更多数据。客户端则获取消息队列的标识符,从标准输入读取数据,并以类型为1的消息发送,最后发送一个类型为2的消息表示数据发送完毕。
1.2 共享内存
共享内存允许两个或多个进程共享一个内存区域,它们可以检查和修改该区域的内容。但在使用共享内存之前,进程需要获取其队列标识符,这可以通过
shmget
函数实现:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int shmflg);
-
size参数指定所需内存段的大小(以字节为单位)。 -
key参数指定该内存段使用的键值,可以是IPC_PRIVATE(总是创建一个新的段)或非零值。 -
shmflg参数用于指定内存段的读写权限,与open和creat函数类似。
成功完成后,
shmget
函数返回共享内存段的标识符;如果段不存在或无法创建,则返回 -1,并通过
errno
描述错误信息。
shmctl
函数可以对共享内存段执行多种控制操作:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
参数是共享内存段的标识符,
buf
参数指向一个
struct shmid_ds
类型的结构,该结构描述了内存段的信息:
struct shmid_ds {
struct ipc_perm shm_perm;
int shm_segsz;
struct anon_map *shm_amp;
ushort shm_lkcnt;
pid_t shm_lpid;
pid_t shm_cpid;
ulong shm_nattch;
ulong shm_cnattch;
time_t shm_atime;
long shm_pad1;
time_t shm_dtime;
long shm_pad2;
time_t shm_ctime;
long shm_pad3;
kcondvar_t shm_cv;
char shm_pad4[2];
struct as *shm_sptas;
long shm_pad5[2];
};
shmctl
函数的
cmd
参数可以取以下值:
| 命令 | 描述 |
| ---- | ---- |
|
IPC_STAT
| 将
struct shmid_ds
结构的当前内容放入
buf
指向的区域。 |
|
IPC_SET
| 将
struct shmid_ds
结构的
shm_perm.uid
、
shm_perm.gid
和
shm_perm.mode
元素更改为
buf
指向区域中的值,此操作仅限于具有超级用户有效用户ID或等于
shm_perm.cuid
或
shm_perm.uid
的进程。 |
|
IPC_RMID
| 从系统中移除指定的共享内存标识符,并销毁与之关联的内存段和数据结构,此命令只能由具有超级用户有效用户ID或等于
shm_perm.cuid
或
shm_perm.uid
的进程执行。 |
|
SHM_LOCK
| 将指定的共享内存段锁定到内存中,只能由超级用户执行。 |
|
SHM_UNLOCK
| 解锁指定的共享内存段,只能由超级用户执行。 |
在进程使用共享内存段之前,需要通过
shmat
函数将其附加到进程的地址空间:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, void *shmaddr, int shmflg);
当程序使用完共享内存段后,可以调用
shmdt
函数将其分离:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(void *shmaddr);
1.3 信号量
信号量不是用于在进程之间交换数据,而是作为计数器,用于在多个进程之间提供对共享数据对象的同步访问。为了获得对共享资源的访问权限,进程需要执行以下步骤:
1. 测试控制该资源访问的信号量的值。
2. 如果值大于零,进程可以使用该资源,并将信号量的值减1,表示正在使用一个单位的资源。
3. 如果信号量的值为零,进程将进入睡眠状态,直到信号量的值大于零,然后返回步骤1。
当进程使用完由信号量控制的共享资源后,信号量的值会加1。如果有进程在步骤3中被阻塞,其中一个进程将被唤醒。大多数信号量是二进制的,其初始值为1,但也可以使用任何正值,该值表示可供共享的资源单元数量。
在使用信号量集之前,进程需要通过
semget
函数获取其标识符:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
semctl
函数可以对信号量集执行多种控制操作:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg);
union semun {
int val;
struct semid_ds *buf;
ushort *array;
};
semop
函数用于操作信号量:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *ops, size_t nops);
struct sembuf {
ushort sem_num;
short sem_op;
short sem_flg;
};
sem_op
的不同取值代表不同的操作:
- 如果
sem_op
为正,其值会加到信号量的值上,对应于释放程序正在使用的共享资源。
- 如果
sem_op
为负,对应于程序想要获取由信号量控制的资源。
- 如果信号量的值大于或等于
sem_op
的绝对值(资源可用),则从信号量的值中减去
sem_op
的绝对值。
- 如果信号量的值小于
sem_op
的绝对值(资源不可用),
semop
要么立即返回错误(如果
IPC_NOWAIT
在
sem_flg
中指定),要么将进程置于睡眠状态,直到信号量的值大于或等于
sem_op
的绝对值。
- 如果
sem_op
为零,
semop
会阻塞,直到信号量的值变为零(除非
IPC_NOWAIT
在
sem_flg
中指定)。
2. 网络编程概念
如今,几乎每个UNIX系统都连接到某种网络。下面介绍一些网络编程的基本概念。
2.1 网络协议
目前,事实上的标准网络协议套件是TCP/IP(传输控制协议/互联网协议),它由互联网工程任务组开发,被全球连接到互联网的主机广泛使用。另一个国际标准协议套件是OSI(开放系统互连),由国际标准化组织(ISO)标准化,虽然在欧洲比较流行,但由于技术和政治等多种原因,在美国并未得到广泛应用。
2.2 主机名和地址
为了在主机之间进行通信,需要指定要通信的主机。人类使用主机名,而程序使用主机地址。
主机名用于区分网络中的每个主机。在私有网络中,主机名可以很简单,如 “fred” 或 “wilma”;但在互联网上,主机名必须是完全限定的域名,如 “fred.some.college.edu” 或 “wilma.company.com”。
互联网域名系统将主机名空间划分为多个逻辑区域或域,这样做有两个主要原因:
- 允许主机名空间的管理分散化,每个组织可以管理自己的名称空间。
- 允许在不同的名称空间区域中重用主机名。
顶级域包括国家的双字母域名,如 “us”(美国)、“se”(瑞典)和 “mx”(墨西哥)。在美国,还有四个其他顶级域:“edu”(教育机构)、“mil”(军事组织)、“gov”(非军事政府组织)和 “com”(商业组织)。
每个顶级域可以进一步细分,例如 “edu” 域可以分为各个学院或大学的域,这些域还可以继续细分,最后细分到主机名。在同一个域内的主机可以使用简单的主机名,但从其他域的主机访问时,必须使用完全限定的域名。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{信号量值 > 0?}:::decision
B -->|是| C(使用资源,信号量值减1):::process
B -->|否| D(进程睡眠):::process
C --> E{是否使用完资源?}:::decision
E -->|是| F(信号量值加1):::process
E -->|否| C
F --> G([结束]):::startend
D --> H{信号量值 > 0?}:::decision
H -->|是| B
H -->|否| D
2.3 套接字接口
在UNIX系统中,套接字接口是实现网络通信的重要手段。之前在进程间通信中介绍过用于同一机器上进程通信的UNIX域套接字,这里将聚焦于用于不同机器上进程通信的Internet域套接字。
所有使用套接字库函数的程序,在Solaris 2.x系统上必须与
-lnsl
和
-lsocket
库链接,在IRIX 5.x系统上则需与
-lnsl
库链接。
2.3.1 套接字编程流程
一般而言,套接字编程主要包含以下步骤,以下以TCP套接字为例:
1.
创建套接字
:使用
socket
函数创建一个套接字描述符。
#include <sys/socket.h>
#include <netinet/in.h>
int socket(int domain, int type, int protocol);
- `domain`:指定协议族,如 `AF_INET` 表示IPv4协议。
- `type`:指定套接字类型,如 `SOCK_STREAM` 表示TCP套接字,`SOCK_DGRAM` 表示UDP套接字。
- `protocol`:通常为0,表示使用默认协议。
-
绑定地址
(服务器端):使用
bind函数将套接字与特定的IP地址和端口号绑定。
#include <sys/socket.h>
#include <netinet/in.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- `sockfd`:套接字描述符。
- `addr`:指向要绑定的地址结构的指针。
- `addrlen`:地址结构的长度。
-
监听连接
(服务器端):使用
listen函数将套接字设置为监听状态,准备接受客户端的连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- `sockfd`:套接字描述符。
- `backlog`:指定允许的最大连接请求队列长度。
-
接受连接
(服务器端):使用
accept函数接受客户端的连接请求,并返回一个新的套接字描述符用于与客户端通信。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- `sockfd`:监听套接字描述符。
- `addr`:指向客户端地址结构的指针。
- `addrlen`:指向客户端地址结构长度变量的指针。
-
发起连接
(客户端):使用
connect函数向服务器发起连接请求。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- `sockfd`:套接字描述符。
- `addr`:指向服务器地址结构的指针。
- `addrlen`:地址结构的长度。
-
数据传输
:使用
send和recv函数在客户端和服务器之间进行数据传输。
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- `sockfd`:套接字描述符。
- `buf`:指向要发送或接收数据的缓冲区的指针。
- `len`:要发送或接收的数据长度。
- `flags`:可选标志。
-
关闭套接字
:使用
close函数关闭套接字,释放资源。
#include <unistd.h>
int close(int fd);
- `fd`:套接字描述符。
2.3.2 地址结构
在套接字编程中,常用的地址结构是
struct sockaddr_in
,用于表示IPv4地址:
#include <netinet/in.h>
struct sockaddr_in {
short sin_family; // 协议族,通常为 AF_INET
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 填充字节,使结构与 struct sockaddr 大小相同
};
struct in_addr {
unsigned long s_addr; // 32位IPv4地址
};
3. 总结
在UNIX系统中,进程间通信和网络编程是非常重要的技术领域。进程间通信机制(如消息队列、共享内存和信号量)为同一计算机上的进程提供了多种数据交换和同步的方式。而网络编程则使得不同计算机上的进程能够进行通信,其中TCP/IP协议和套接字接口是实现网络通信的关键。
在实际应用中,我们需要根据具体的需求选择合适的进程间通信机制和网络编程方法。例如,对于简单的数据交换,消息队列可能是一个不错的选择;而对于需要高效共享数据的场景,共享内存则更为合适。在网络编程方面,TCP套接字适用于需要可靠传输的场景,而UDP套接字则更适合对实时性要求较高、对数据可靠性要求相对较低的场景。
| 技术类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 消息队列 | 进程间数据交换 | 简单易用,支持不同进程间通信 | 有一定的开销,不适合大量数据传输 |
| 共享内存 | 高效数据共享 | 速度快,适合大量数据共享 | 需要额外的同步机制 |
| 信号量 | 进程同步 | 提供同步访问,避免资源竞争 | 实现相对复杂 |
| TCP套接字 | 可靠数据传输 | 保证数据可靠到达,按序传输 | 建立连接和断开连接开销大,实时性相对较差 |
| UDP套接字 | 实时数据传输 | 开销小,实时性好 | 不保证数据可靠到达,可能会丢包 |
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A([开始]):::startend --> B(创建套接字):::process
B --> C{服务器或客户端?}
C -->|服务器| D(绑定地址):::process
C -->|客户端| E(发起连接):::process
D --> F(监听连接):::process
F --> G(接受连接):::process
G --> H(数据传输):::process
E --> H
H --> I(关闭套接字):::process
I --> J([结束]):::startend
通过深入理解这些技术的原理和使用方法,我们可以更好地开发出高效、稳定的UNIX系统应用程序。
超级会员免费看
4765

被折叠的 条评论
为什么被折叠?



