APUE 第15-16章 进程间通信

第15章 进程间通信

管道

限制:管道应当被认为是半双工的;只能在具有公共祖先的进程间使用。

int pipe(int fd[2])

fd[0]为读打开,fd[1]为写打开,fd[1]的输出是fd[0]的输入。

先建立父进程的pipe,fork之后关闭父进程和子进程的各一fd来实现进程间通信

FILE * popen ( const char * command , const char * type );
int pclose ( FILE * stream );

popen() 函数通过创建一个管道,调用fork产生一个子进程,执行一个shell以运行命令来开启一个进程。这个进程必须由pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose() 返回的终止状态与 shell 已执行 exit 一样。

type 参数只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。

command 参数是一个指向以NULL结束的shell命令字符串的指针。这行命令将被传到bin/sh并使用-c标志,shell将执行这个命令。

FIFO

就是命名的管道,因为有名字,所以可以被多个进程写。使用mkfifo, mkfifoat创建,并用open打开。

类似于管道,write一个尚未有进程为读而打开的FIFO,产生信号SIGPIPE;若一个FIFO的最后一个写进程关闭了该FIFO,读进程将读到一个文件结束标志。

FIFO不会创建中间临时文件。

XSI IPC

有3种被称作XSI IPC的IPC:消息队列、信号量和共享存储器。它们有很多相似之处。

每个IPC结构都有一个非负整数的标识符(identifier),是其内部名。每个IPC对象都与一个键(key)相关联,这是外部名。

有多种方法可以使客户进程和服务器进程在同一IPC结构汇聚:
1. 服务器用键IPC_PRIVATE创建一个新IPC结构,并将返回的标识符存放在文件中。客户进程则去相应位置取出这个标识符。如果对于父子进程,只需要一个变量就可以做到。这种技术需要对文件系统的调用。

键IPC_PRIVATE保证创建一个新的IPC。
2. 在一个公用头文件定义一个键,服务器创建客户端访问。但是可能会有冲突,如果该键已被使用的话。
3. 客户和服务器都认同一个路径名和项目ID,利用ftok函数将两者转换成一个键,但仍然有冲突风险。

3个get函数:msgget,semget,shmget用于创建新的IPC或访问已有IPC。它们有两个参数:key和flag。如果要创建,则flag为IPC_CREAT;指定现有IPC时,且key必须等于那个key,IPC_CREAT必须不被指定。

不可IPC_PRIVATE来引用一个现有IPC。

如果希望创建一个新IPC,而且要确保没有引用具有同一标识符的现有IPC,则必须同时指定IPC_EXCL和IPC_CREAT,这样的话,出现上述情况就会返回EEXIST(这与指定了O_CREAT和O_EXCL的open类似)。

缺点:
1. IPC在进程终止后将一直存在,直到消息被删除或IPC被删除
2. IPC在文件系统中没有名字,内核必须增加全新的系统调用来操作它。

消息队列

每个消息队列有一个msqid_ds结构与之相关联,存储了队列的相关信息。

msgget创建新队列或打开已有队列,msgctl对队列执行一些操作,msgsnd将数据放到队列尾端,msgrcv则从队列取用消息,它的参数type可以指定想要哪一种消息,这允许以非先入先出的次序读消息。

信号量

信号量应当被设计为原子的,所以应当是系统调用。但是XSI信号量很复杂,带来了以下缺陷:
1. 信号量不是单个非负值,而定义为含有一个或多个信号量值的集合
2. 信号量的创建和初始化是两步,这导致不能原子地创建,致命
3. 没有进程使用的信号量仍然存在而没有被释放。

类似的,内核为每个信号量集合维护一个semid_ds结构;

每个信号量用一个无名结构表示。semget创建或绑定一个, semctl初始化或获取信息, semop对信号量集进行原子性的操作。

共享存储

和mmap类似,只是不需要一个文件了。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

内核为每个共享内存区维护一个shmid_ds结构。

shmget创建一个或者获取一个共享存储;shmctl设置和查询相应共享存储的属性;shmat将一个共享存储段映射到一个地址空间(为了兼容性,一般选择让内核来选择可用地址而不是自己指定);shmdt将共享存储段分离,但它并不消亡,直到用shmctl显式删除。

mmap实现共享存储:MAP_SHARED 指明对映射区数据的修改,多个共享该映射区的进程都可以看见,而且会反映到实际的文件。

POSIX信号量

POSIX信号量有两种:有名信号量和无名信号量,无名信号量也被称作基于内存的信号量。有名信号量通过IPC名字进行进程间的同步,而无名信号量如果不是放在进程间的共享内存区中,是不能用来进行进程间同步的,只能用来进行线程同步。

POSIX信号量有三种操作:
1. 创建一个信号量。创建的过程还要求初始化信号量的值。
根据信号量取值(代表可用资源的数目)的不同,POSIX信号量还可以分为:
二值信号量:信号量的值只有0和1,这和互斥量很类型,若资源被锁住,信号量的值为0,若资源可用,则信号量的值为1;
计数信号量:信号量的值在0到一个大于1的限制值(POSIX指出系统的最大限制值至少要为32767)。该计数表示可用的资源的个数。
2. 等待一个信号量(wait)。该操作会检查信号量的值,如果其值小于或等于0,那就阻塞,直到该值变成大于0,然后等待进程将信号量的值减1,进程获得共享资源的访问权限。这整个操作必须是一个原子操作。该操作还经常被称为P操作(荷兰语Proberen,意为:尝试)。
3. 挂出一个信号量(post)。该操作将信号量的值加1,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒。该操作也必须是一个原子操作。该操作还经常被称为V操作(荷兰语Verhogen,意为:增加)

有名信号量

有名信号量的创建和删除

sem_t *sem_open(const char *name, int oflag);  
sem_t *sem_open(const char *name, int oflag,  mode_t mode, unsigned int value);  
                              //成功返回信号量指针,失败返回SEM_FAILED  

sem_close用于关闭打开的信号量。当一个进程终止时,内核对其上仍然打开的所有有名信号量自动执行这个操作。调用sem_close关闭信号量并没有把它从系统中删除它,POSIX有名信号量是随内核持续的。即使当前没有进程打开某个信号量它的值依然保持。直到内核重新自举或调用sem_unlink()删除该信号量。

sem_unlink用于将有名信号量立刻从系统中删除,但信号量的销毁是在所有进程都关闭信号量的时候。

int sem_close(sem_t *sem);  
int sem_unlink(const char *name);  
                              //成功返回0,失败返回-1  

sem_wait()用于获取信号量,首先会测试指定信号量的值,如果大于0,就会将它减1并立即返回,如果等于0,那么调用线程会进入睡眠,指定信号量的值大于0.

sem_trywait和sem_wait的差别是,当信号量的值等于0的,调用线程不会阻塞,直接返回,并标识EAGAIN错误。

sem_timedwait和sem_wait的差别是当信号量的值等于0时,调用线程会限时等待。当等待时间到后,信号量的值还是0,那么就会返回错误。其中 struct timespec *abs_timeout是一个绝对时间,具体可以参考条件变量关于等待时间的使用

int sem_wait (sem_t *sem);  

#ifdef __USE_XOPEN2K  
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);  
#endif  

int sem_trywait (sem_t * sem);  
                              //成功返回0,失败返回-1  

当一个线程使用完某个信号量后,调用sem_post,使该信号量的值加1,如果有等待的线程,那么会唤醒等待的一个线程。

int sem_post(sem_t *sem);  
                            //成功返回0,失败返回-1  

该函数返回当前信号量的值,通过sval输出参数返回,如果当前信号量已经上锁(即同步对象不可用),那么返回值为0,或为负数,其绝对值就是等待该信号量解锁的线程数。

int sem_getvalue(sem_t *sem,  int *sval);  
                            //成功返回0,失败返回-1  
无名信号量

sem_init()用于无名信号量的初始化。无名信号量在初始化前一定要在内存中分配一个sem_t信号量类型的对象,这就是无名信号量又称为基于内存的信号量的原因。

sem_init()第一个参数是指向一个已经分配的sem_t变量。第二个参数pshared表示该信号量是否由于进程间通步,当pshared = 0,那么表示该信号量只能用于进程内部的线程间的同步。当pshared != 0,表示该信号量存放在共享内存区中,使使用它的进程能够访问该共享内存区进行进程同步。第三个参数value表示信号量的初始值。

int sem_init(sem_t *sem, int pshared, unsigned int value);  
                            //若出错则返回-1  
int sem_destroy(sem_t *sem);  
                            //成功返回0,失败返回-1  

进程间通信:套接字

使用socket函数打开一个套件字,会产生一个套接字描述符,在UNIX中被认为是一种文件描述符,很多处理文件描述符的函数可以处理它。

int socket(int domain, int type, int protocol);

domain确定通信的特性,type确定套接字的类型,protocol通常为0,表示为给定的域和套接字类型选择默认协议。

type:
- SOCK_DGRAM:无连接的数据报,UDP
- SOCK_STREAM:TCP,基于字节流的可靠传输
- SOCK_SEQPACKET:类似TCP,但是基于报文,保证顺序到达。SCTP
- SOCK_RAW:用于直接访问网络层,应用负责构造自己的协议头部

shutdown用于关闭socket的读口或写口。

close可以关闭一个套接字,但因为可以使用dup复制套接字(复制一个描述符指向同一套接字),那么直到close了最后一个引用套接字的fd才会释放。所以使用shutdown,它会让一个套接字本身处于不活动状态,与引用它的fd数目无关,而且可以控制一个方向。

TCP/IP协议
使用大端字节序,所以从主机传数据有时需要进行转换。使用htonl, htons, ntohl, ntohs进行转换,h表示主机字节序,n表示网络字节序,l表示长整型,s表示短整型。

为使不同格式地址能够传入到套接字函数,地址被强制转换成一个通用的地址结构sockaddr,无论是IPV4还是IPV6。

inet_ntop将网络字节序的二进制地址转换成文本字符串格式, inet_pton则反之。

地址查询

ent是entry的简写,表示记录,以下的get函数用于打开,set函数用于打开,如果已经打开则回绕,end用于关闭

顺序扫描主机信息:gethostent, sethostent, endhostent

获得网络,顺序扫描网络名字和网络编号:getnetbyaddr, getnetbyname, getnetent, setnetent, endnetent

获得协议,顺序扫描协议数据库:getprotobyname, getprotobynumber, getprotoent, setprotoent, endprotoent

获得服务,顺序扫描服务数据库:getservbyname, getserbyport, getservent, setservent, endservent

将一个主机名和一个服务名映射到一个地址,释放,处理出错:getaddrinfo,freeaddrinfo,gai_strerror

将一个地址转换为一个主机名和一个服务名:getnameinfo

利用bind将一个地址和套接字绑定,利用getsockname发现绑定到套接字上的地址,利用getpeername发现绑定到对等方套接字的地址。

建立连接

利用connect建立连接,要求服务器必须打开,必须绑定到一个想与之连接的地址上,且等待连接队列要有空位。

connect可用于UDP,这似乎不太合理,实际上比较有用,这个函数相当于指定了目标地址,这样每次传报文就不需要提供地址,而且这条连接当然只能接收到来自指定地址的报文

服务器调用listen监听并用accept接收连接。accept返回一个新的套接字描述符,连接到调用connect的客户端,而传给accept的原始套接字继续保持接收其他连接。

accept会阻塞一直到一个请求到来。

数据传输

可以通过read和write操作套接字,但是功能有限,如果需要更多功能,有6个专用函数。

send:和write类似,但支持一个flag参数,允许一些高级操作;sendto可以带一个地址,是发送报文的一种方式。

sendmsg类似writev,用于顺序发送多个缓冲区的数据

recv类似read。recvfrom可以得到发送者的源地址,通常用于无连接的套接字。

recvmsg类似于readv。

套接字选项

利用setsockopt设置套接字选项,利用getsockopt查看当前选项。

可以设置套接字是否广播,设置接收区、发送区的缓冲大小,启用keep-alive,设置超时值,允许重用bind中的地址等属性。

一个套接字由:协议,本地地址,本地端口,远程地址,远程端口组成,任何一个不同就是不同的套接字。

带外数据

out-of-band data,是一些通信协议所支持的选项,比普通数据有更高的传输优先级。TCP支持,并称之为紧急数据(urgent data),只有一个字节,UDP不支持。

在send函数中使用MSG_OOB标志来指定紧急数据。收到该数据会发送SIGURG信号。

TCP支持紧急标记(urgent mark),将紧急数据放在普通数据流中。可以在套接字选项中使用SO_OOBINLINE来达到这个效果。

将套接字设为非阻塞模式后,如果不能立即进行IO,则出错返回,可以利用select、poll实现非阻塞IO。另外,可以通过一些操作,实现套接字的异步IO。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值