对于CS的课程,除了离散数学和编译原理这种抽象程度较高的理论课,其他的基本都要自己动手写代码、观察和调试的,这里的读书笔记其实没啥大用,只是作为一个督促自我学习的方式,同时也算是以后方便查询的index吧。马上就要开学,华为的习题还没做,今天开始添加此项任务吧。
IPC方式,POSIX.1规定的有pipe和FIFO;XSI扩展中又添加了消息队列、信号量和共享内存,以及网络编程中的套接字;XSI可选部分规定了STREAMS流机制。
管道:
最古老的IPC方式,POSIX.1规定的是半双工,某些系统提供全双工实现。
只能在具有公共祖先的进程间使用,创建:
#include <unistd.h>
int pipe(int fields[2]); //fields[0]为读端口,fields[1]为写端口;
管道是文件,可以用S_ISFIFO来判断,fstat对管道的每一端都返回一个FIFO类型的文件描述符;
常见用法:pipe创建管道,fork创建子进程;在父进程中关闭fd[0],在子进程中关闭fd[1];父进程用于写,子进程用于读;或者反着来,子进程写父进程读;
更常见的用法:直接复制文件描述符为对应的标准输入/输出;
若管道的一端关闭,规则:
写读端已关闭的管道,产生信号SIGPIPE,返回-1,errno=EPIPE;
读写端已关闭的管道,正常;若读取完毕,read返回0,表示到达文件尾端;
常量PIPE_BUF规定了管道缓冲区的大小,若write的字节数大于该常量,那么,可能会造成多个同时写该管道(或FIFO)的进程之间出现穿插。
管道可用于父子进程之间的同步,简单来说,创建两条管道(TELL_WAIT),然后等待(read);完成操作后发送(write);
由于复制为标准I/O的用法太过常见,因此标准库直接提供了更简单的函数:
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type); //type=r,w,表示为读|写而打开管道;分别将其标准输出|输入连接到文件指针;
int pclose(FILE *fp); //关闭I/O流,返回shell结束状态
popen会先fork然后exec参数中的程序,返回打开文件的指针。
popen绝不应该由setuid | setgid的程序调用;
popen特别适用于构造简单的过滤器程序。
协同进程
当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出,则称之为该过滤程序的协同程序。
通常采用两个管道实现协同程序。注意标准I/O对于管道是全缓冲机制的。
FIFO
又称为命名管道,与管道的最大不同在于可以由非公共祖先的进程共同使用。
FIFO类似于普通文件,不同进程之间通过援引文件名来使用FIFO。创建:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
mode的参数和创建文件时权限的参数相同;一旦创建FIFO完毕,其使用方法类似于pipe和普通文件。
常见用法:FIFO用于客户进程—服务器进程之间的通信,客户进程通过众所周知的FIFO向服务器进程发送read请求,服务器进程通过各自的FIFO响应客户进程;
XSI IPC
XSI IPC都通过"标识符"加以引用,IPC标识符一般是long类型的整数,其外部名被称为key,共享IPC数据结构的方案:
- 服务器进程在使用创建IPC的函数时,指定key=IPC_PRIVATE用于创建一个新IPC结构,将返回的标识符存放在一个文件中以便客户进程取用,
- 在一个公用的头文件中指定key,然后服务器用此key创建结构。缺点是可能已经被占用了。
- 指定一个路径名和项目ID(0~255),使用ftok生成key(注意路径名必须现实存在)。注意:如果使用同一ID,那么key可能重复;
创建IPC的函数有相同的结构(msgget, semget, shmget),第一个参数是key,第二个则是flag;
创建新IPC的方法:key=IPC_PRIVATE,或flag中指定IPC_CREAT(最好同时指定IPC_EXCL,防止打开了一个已创建的同名IPC结构)。
权限结构,所有XSI IPC中都有一个结构成员来限制IPC的权限:
struct ipc_perm{
uid_t uid; //owner's uid
gid_t gid; //owner's gid
uid_t cuid; //creator's uid
gid_t cgid; //creator's gid
mode_t mode;//access mode
};
除了不可执行外,mode与普通文件的访问权限一致;
XSI IPC的主要缺点是没有访问计数,必须显式删除;其二是这些IPC结构在文件系统中没有名字,这意味着必须创建一系列的系统调用来完成对IPC对象的处理;
消息队列
每则消息由三部分组成:type(unsigned long),length(unsigned, bytes)和实际的消息。
struct msqid_ds{
struct ipc_perm msg_perm; //权限控制
msgnum_t msg_qnum; //队列中消息
pid_t msg_lspid; //pid of last msgsnd();
pid_t msg_lrpid; //pid of last msgrcv()
time_t msg_stime; //last msgsnd() time
time_t msg_rtime; //last msgrcv() time
time_t msg_ctime; //last-change time
};
相关限制:消息最长字节|队列的最大字节等;
处理函数:
#include <sys/msg.h>
int msgget(key_t key,int flag);//返回消息ID或-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//垃圾桶函数,cmd=IPC_STAT(取队列的msqid_ds结构,存放在buf中),IPC_SET(设置该队列的),IPC_RMID(删除该消息队列和数据)
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);//flag可指定IPC_NOWAIT(无阻塞)
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);//type指定消息类型,type=0返回第一条消息;type<0则返回队列中消息类型值小于或等于type绝对值的消息(取最小的一个)
信号量
semaphore 是一种计数器,用来对可用资源进行计数。理论上说,很简单:
如果资源可用,那么信号量的值>0,每使用一个资源,信号量-1;
如果资源不可用,那么信号量=0,进程休眠等待;当信号量>0时,进程被唤醒;
如果进程不再使用资源,那么释放,信号量+1.
信号量集:
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems;
time_t sem_otime;
time_t sem_ctime;
};
信号量:
struct{
unsigned short semval;
pid_t sempid;
unsigned short semncnt;
unsigned short semzcnt;
};
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
int semctl(int semid, int semnum, int cmd,… /* union semun arg */);
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
除了和msg类似的3个cmd外,还有一系列用于获取参数的cmd(GETVAL,GETPID等);
int semop(int semid, struct sembuf semoparray[], size_t nops);
struct sembuf{
unsigned short sem_num; //信号集中的信号序数
short sem_op; //操作(加减)
short sem_flag; //IPC_NOWAIT, SEM_UNDO
};
SEM_UNDO存在的意义在于可以在exit后内核可以自己处理信号量,进行收尾工作。
一般而言,使用记录锁可以完成的任务,最好不要使用更为复杂的信号量;
共享内存
最快的IPC,回忆进程的内存空间结构,共享内存在stack的地址前,heap的地址后(向上增长)。内核实际链接的共享存储段放在进程的什么位置和系统明确相关。
使用共享存储需要注意多个进程之间的访问同步问题。
各种处理函数和flag与前面两种IPC大致相同。
注意使用shmat链接到段,使用shmdt脱离段,使用shmctl删除段。
文中给出了两个特殊实例:
将/dev/zero映射用mmap映射,并指定flag为MAP_SHARED,那么多个进程可以共享此文件映射存储区。好处是无须存在一个实际文件(如果仅仅是为了使用共享存储),而且比较简单,但是只能在相关进程中起作用;除此之外,匿名存储映射也可以完成类似的功能(更加简单)。