title: linux/unix编程手册-46_50 date: 2018-10-01 11:53:07 categories: programming tags: tips
linux/unix编程手册-46(System V 消息队列)
消息队列和FIFO比较
- msgget()返回的标识符不同于传统I/O的文件描述符
- 面向消息不同于字节流
- 消息有整型表示的类型,可以根据类型读取
创建或打开一个消息队列(略,45章覆盖了)
交换消息和控制消息
#include<sys/types.h>
#include<sys/msg.h>
struct mymsg{
long mtype;
char mtext[];
};
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//return 0 successl-1 error
ssize_t msgrcv(int msqid, const void *msgp, size_t maxmsgsz,long msgtyp, int msgflg);
// return bytes copied to msgp.mtext, -1 error
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//return 0 success, -1 error
复制代码
msgsnd
- msgflg目前只定义了IPC_NO_WAIT(执行非阻塞发送,如果队列满了,会返回EAGAIN)
- msgsnd需要写权限
- msgsz指定mtext包含字节数
msgrcv
- 如果mtext>maxmsgsz,不会删除消息同时默认返回E2BIG
- msgtyp
- 等于0: 删并返回第一条
- 大于0: 匹配msgtyp的第一条,删并返回(一般可以指定成进程ID)
- 小于0: 会当成优先队列处理,选取队列中msgtyp最小并且<=|msgtyp|的第一条消息,删除并返回
- msgflg
- IPC_NOWAIT 类似
- MSG_EXCEPT (linux 特有
#define _GNU_SOURCE
)作为对msgtyp的补充,匹配不等于msgtyp的消息- MSG_NOERROR 当果mtext>maxmsgsz,会截断mtext至maxmsgsz,删除消息并返回
msgctl
- cmd
- IPC_RMID:删除消息队列对象和关联的msqid_ds数据结构,会忽略第三个参数,阻塞的读写进程会立刻醒来,msgsnd和msgrcv返回EIDRM
- IPC_STAT:将消息队列关联的数据结构的副本放到buf
- IPC_SET: 用buf更新关联的msqid_ds结构中被选中的字段
- IPC_INFO:(linux),同时buf 为 msginfo类型,返回消息队列的各种限制 msgctl(0, IPC_INFO, (struct msqid_ds) &buf)*
- MSG_INFO:(linux,SEM_INFO|SHM_INFO),同时buf 为 msginfo类型,返回消息队列的资源消耗,返回的是entries的最大项下标
- MSG_STAT:(linux,SEM_STAT,SHM_STAT),第一个参数为entries的下标,返回ipc对象标识符 如果阻塞时msgsnd和msgrcv被信号处理器中断,调用失败并且返回EINTR,且不会自动重启,不论信号处理是否设置了
SA_RESTART
标记
消息队列关联数据结构
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd() */
time_t msg_rtime; /* Time of last msgrcv() */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Number of bytes in queue */
msgqnum_t msg_qnum; /* Number of messages in queue */
msglen_t msg_qbytes; /* Maximum bytes in queue */
pid_t msg_lspid; /* PID of last msgsnd() */
pid_t msg_lrpid; /* PID of last msgrcv() */
};
复制代码
消息队列的限制
- MSGMNI:消息队列的数量
- MSGMAX:单条消息最多可入字节数
- MSGMNB:消息队列msg_qbytes最大值
- MSGTQL:消息总数
- MSGPOOL:消息队列数据缓冲池的大小
缺点
- 不是通过文件描述的引用,epoll等无法应用在消息队列上,如果代码里也是用文件描述符,会导致复杂
- 用键而不是文件名标识消息使设计复杂
- 消息队列无连接,内核对它没有引用计数
- 如何能够安全删除消息队列
- 如何确保消息队列不被删除
- 一般使用posix消息队列替代system v消息队列
- 更深一点使用基于多文件描述符的通信模型,ex:
FIFO/SOCKET + epoll
linux/unix编程手册-47(System V 信号量)
信号量是用于同步进程动作,而不是进程间传递数据的
在一个信号量上的操作
- 将信号量设置成一个绝对值
- 当前值基础上加
- 当前值基础上减(可能阻塞,内核不允许信号量小于0)
- 等待信号量的值为0(可能阻塞)
创建或打开一个信号集
#include<sys/types.h>
#include<sys/sem.h>
int semget(key_t key, int nsems, int semflg);
// nsems:信号集中信号量的数量
复制代码
信号量的控制
#include<sys/types.h>
#include<sys/sem.h>
union semnum {
int val;
struct semid_ds buf;
unsigned short * array;
#if defined(__linux__)
struct seminfo * __buf;
#endif
};
int semctl(int semid, int semnum, int cmd, .../* union semun arg */);
// semnum标明了集合的具体哪个信号量
复制代码
cmd
- IPC_RMID:删除
- IPC_STAT:将semid_ds拷贝到arg.buf
- IPC:SET:arg.buf替换
- GETVAL:获取第semnum个信号量的值
- SETVAL:将第semnum个信号量的值设为arg.val
- GETALL:获取所有信号量的值放到arg.array指向的数组中
- SETALL:获取所有信号量的值设置成arg.array指向的数组的值
- GETPID:函数会返回上一个在该信号量上执行semop()进程的id,没有则0
- GETNCNT:函数会返回等待该信号量值增长的进程数
- GETNCNT:函数会返回等待该信号量值置为0的进程数
后面6个cmd在返回时可能已经过期
信号量关联数据结构
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Time of last semop() */
time_t sem_ctime; /* Time of last change */
unsigned long sem_nsems; /* Number of semaphores in set */
};
复制代码
信号量初始化
- linux上
semget
创建的集合中的信号量会默认初始化0,但为了移植性,需要在semctl显式的初始化
信号量操作
#include<sys/types.h>
#include<sys/sem.h>
struct sembuf {
unsigned short sem_num; /* Semaphore number */
short sem_op; /* Operation to be performed */
short sem_flg; /* Operation flags (IPC_NOWAIT and SEM_UNDO) */
};
int semop(int semid, struct sembuf *sops, unsigned int nsops);
//return 0 succes, -1 error
#define _GNU_SOURCE
int semtimedop(int semid, struct sembuf *sops, unsigned int nsops, struct timespec *timeout);
复制代码
- sops[i].sem_op
- =0,检查现在是不是0,不是阻塞到变成0
- !=,+sem_op(可正可负,负可能阻塞)值
- sem_flg
- 设置IPC_NOWAIT后,原来需要阻塞会返回EAGAIN
- 设置SEM_UNDO,内核会记录一个进程在一个信号量上使用SEM_UNDO操作做出的调整总和,进程终止时减去这个总和
- 异常,当执行减去这个操作后,信号量值<0:
- 阻塞(不可行)
- 尽可能减少(到0)并退出(linux的策略)
- 直接退出不执行操作(部分unix实现)
- 如果多个的话,操作是原子的,一个阻塞整个操作都会阻塞
多个阻塞信号量操作的处理
- A让信号量减2,B让信号量减1,C使信号量加1,A会最后才会不阻塞
- A让信号量1,2都减1,,B让信号量1减1,C使信号量1加1,A会最后才会不阻塞
信号量的限制(一些常量,用的会比较少吧)
信号量的缺点
- 和消息队列差不多System V IPC的共同的缺点
- 替代方案较少,ex:记录锁
linux/unix编程手册-48(System V 共享内存)
相较于消息队列和信号量,共享内存无需将数据在用户空间和内核空间传递
创建或打开一个共享内存段
#include<sys/types.h>
#include<sys.shm.h>
int shmget(key_t key, size_t size, int shmflg);
// size 是分配的字节数,创建时会自动被提升到最近系统分页的整数倍
复制代码
shmflg在linux上还有一些特殊的值
- SHM_HUGETLB:特权进程可以通过此创建巨页的共享内存,减少TLB的条目数量
- 在虚拟内存管理中,内核维护一个将虚拟内存地址映射到物理地址的表,对于每个页面操作,内核都需要加载相关的映射。如果你的内存页很小,那么你需要加载的页就会很多,导致内核会加载更多的映射表。而这会降低性能。使用“大内存页”,意味着所需要的页变少了。从而大大减少由内核加载的映射表的数量。这提高了内核级别的性能最终有利于应用程序的性能。简而言之,通过启用“大内存页”,系统具只需要处理较少的页面映射表,从而减少访问/维护它们的开销!
- SHM_NORESERVE:(略)
使用共享内
#include<sys/types.h>
#include<sys.shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
//return address if success,附加
int shmdt(onst void *shmaddr);
//0 success, -1 error, 分离
复制代码
shmaddr
- 为NULL,段会被附加到内核选择的一个合适的地址处(通常推荐做法)
- 不为NULL,shmflg没有设置SHM_RND掩码位,会被附加到shmaddr地址处,地址必须是系统分页大小整数倍,不然EINVAL
- 不为NULL,设置了掩码位,地址会被舍入到最近的常亮SHMLBA(和分页大小的区别?)的倍数,通常SHMLBA是系统分页大小的某个倍数,保证同一段的不同附加操作在CPU快速缓冲中不会不一致(???)
一个进程附加一个共享内存需要读写权限,在shmflg可指定SHM_RDONLY,自己限制只有读权限
fork()时创建的子进程会继承父进程的共享内存段 exec()时,所有附加内存段会被分离,进程终止之后也会被分离
共享内存在虚拟内存的位置
- 共享内存段的虚拟地址从TASK_UNMAPPED_BASE(32为通常为0x40000000) 定义的位置开始
/proc/PID/maps
可以看到程序映射的共享内存段和共享库的位置
共享内存中存储指针
共享内存中存储的只能是指针地址的相对偏移量(不同附加的虚拟地址是不一致的),解引用时也需要补上偏移量
共享内存控制操作
#include<sys/types.h>
#include<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
复制代码
类似前两个
共享内存关联数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment in bytes */
time_t shm_atime; /* Time of last shmat() */
time_t shm_dtime; /* Time of last shmdt() */
time_t shm_ctime; /* Time of last change */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat() / shmdt() */
shmatt_t shm_nattch; /* Number of currently attached processes */
};
复制代码
共享内存的限制(略)
linux/unix编程手册-49(内存映射)
概述
mmap()
系统调用再调用进程的虚拟地址空间创建一个新的内存映射
分类
- 文件映射
- 匿名映射:分页会初始化0
多个进程共享内存映射共享的情形
- 映射了同一文件同一区域
- fork创建子进程
- 私有映射:映射内容发生变更对其他进程不可见,内核使用了copy-on-write实现,写的时候会创建新的分页,将修改写到上面
- 共享映射:映射内容发生变更对其他进程可见,文件映射来讲变更会发生在底层文件上
- fork 继承,exec解除
'/proc/PID/maps'查看内存映射的信息
#include<sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
//return 映射内存的开始地址,或者MAP_FAILED出现异常
复制代码
- addr: NULL时自动分配,其他时作为参考, flag包含MAP_FIXED时,addr必须分页对齐
- lenght:分配字节数会被提升到分页大小的下个整数倍
- prot:掩码,等于PROT_NONE(区域无法访问,用作守护分页?)或者PROT_READ,PROT_WRITE,PROT_EXEC的组合
- flag:MAP_PRIVATE,MAP_SHARED
- fd,offset用于文件系统
- fd打开的文件权限需要和prot和flags参数值匹配(如果指定了PROT_WRITE和MAP_SHARED,需要写权限)
- fstat获取下文件的字节数一般会这么操作
- mmap之后可以关闭文件描述符(影响写么?)
- SUSv4:放宽了要求
- offset不需要时分页大小的倍数
- 指定了MAP_FIXED, 要求addr不为0时分页对齐,或者addr和offset除以系统分页大小所得余数相等
解除映射区域
#include<sys/mman.h>
int munmap(void *addr, size_t length);
//return 0 succes, -1 fail
// 如果addr+lenght范围不存在内存映射,返回0什么都不做
复制代码
文件映射
私有文件映射
- 用例(一般由程序加载器和动态连接器创建)
- 允许执行同一程序或通一个共享库的进程共享同样的只读文本段
- 一个一个可执行文件或共享库的初始化数据段
共享文件映射
- 内存映射I/O
- 是read和write的替代方案,比较
- 正常的read,write需要两次传输,一次在文件和内核高速缓冲区,一次是高速缓冲区和用户缓冲区,mmap之后无需第二次传输
- 正常的read,write会在用户空间和内核空间的缓冲区分别保存,mmap之后内核空间和用户空间共享一个缓冲区
- 大型文件重复执行随机访问会提高较多性能
- 作为共享文件映射的IPC
- 和SYSTEM V 共享内存对象的区别:
- 变更会反映到映射文件,遭遇重启时可以持久化
边界情况
- 存在三个值,文件的字节数A,length的字节数B,向上舍入后的分配的字节数C,向上舍入的分页为D
- 访问>C,并且不是别的映射的字节,会导致SIGSEGV错误
- 当扩充超过了文件结尾时,即D区域不包含文件的字节时如下
同步映射区域
#include<sys/mman.h>
int msync(void *addr, size_t length, int flags);
// 0 s, -1 f
复制代码
flags
- MS_SYNC:阻塞到同步完成
- MS_ASYNC
- MS_INVALIDATE:时映射数据缓存副本无效,当内存中所有修改同步到文件后,不一致的分页标记无效,下次引用时会拉取更新(用于使其他进程对写入立即可见)
其他mmap()标记
匿名映射
- 创建的方法
- flags指定MAP_ANONYMOUS,并且fd=-1
- 打开/dev/zero设备文件,将描述符传给mmap()
重新映射一个映射区域:mremap()
(略)
MAP_NO_RESERVE和过度利用交换空间(略,查一下overcommit_ratio)
OOM killer
/proc/PID/oom_adj
中oom_score, -16~15,越大越容易被杀掉,-17的话不会被杀
非线性映射和MAP_FIXED标记(略)
其他/proc/$PID/maps保存进程的内存映射
address perms offset dev inode pathname
08048000-08056000 r-xp 00000000 03:0c 64593 /usr/sbin/gpm
复制代码
perms:s=share,p=private
linux/unix编程手册-50(虚拟内存操作)
改变内存保护
#include<sys/mman.h>
int mprotect(void *addr, size_t length, int prot);
// 0 s, -1 e
复制代码
内存锁
将虚拟内存部分或全部锁进物理内存,系统调用 mlock 家族允许程序在物理内存上锁住它的部分或全部地址空间。这将阻止Linux 将这个内存页调度到交换空间(swap space),即使该程序已有一段时间没有访问这段空间
#include<sys/mman.h>
int mlock(void *addr, size_t length);
int munlock(void *addr, size_t length);
int mlockall(int flags);
int munlockall(void);
#define _BSD_SOURCE
int mincore(void *addr, size_t length, unsigned char *vec);
// 0 s, -1 e
// 从addr下面的一个分页开始锁住length字节分页
复制代码
略其他