title: linux/unix编程手册-41_45 date: 2018-09-09 11:53:07 categories: programming tags: tips
linux/unix编程手册-41(共享库基础)
静态库(归档文件)
- 将一组常用的目标文件组织进单个库文件,之后应用程序使用相应代码无需重新编译(无需
include
)- 链接命令变简单了,
ld
时只需要指定静态库名称
创建和维护静态库
ar options archive object-file...复制代码
options
取值
- r(替换)
$ cc -g -c mod1.c mod2.c mod3.c$ ar r libdemo.a mod1.o mod2.o mod3.o$ rm mod1.o mod2.o mod3.o复制代码
- t(加v显示文件所有特性类似ll)
$ ar t(v) libdemo.amod1.o mod2.o mod3.o复制代码
- d(从归档文件删除一个模块)
$ ar d libdemo.a mod3.o复制代码
使用静态库
cc -g -o prog prog.o libdemo.a/**如果在链接器搜索的标准目录中linux一般是/lib, /usr/lib, 可能/lib64,可以省略lib和a*libdemo.a -> ldemo*若目录不存在搜索中L指定*libdemo.a -> -Lmylibdir ldemo*/复制代码
共享库概述
静态库的缺点
- 可执行文件存储多份目标模块代码,浪费磁盘
- 运行时,每个程序会在虚拟内存中保存一份目标模块的副本,浪费虚拟内存使用
- 修改时所有用到目标模块的可执行文件需要重新链接
共享库解决了以上问题同时
- 某些情况下大型代码可以完全背加载进内存,可以快速启动;第一个加载共享库的文件会启动的慢些
- 满足一定条件下,修改目标模块时,运行着该目标模块的程序也可以进行这项变更
- 创建和构建共享库会更加复杂
- 共享库编译时必须要使用位置独立的代码(大多数架构下需要使用一个额外的寄存器,记录独立路径?)
- 在运行时必须执行符号重定位,将共享库中的每个符号(变量或函数)的引用修改成符号在虚拟内存的实际运行时的位置,产生一定的花费(how?)
创建共享库(ELF格式)
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c$ gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.oor$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so复制代码
-fPIC
选项指定编译器生成位置独立的代码,会影响编译器生成特定代码的方式,包括全局,静态,外部变量,字符常量,函数地址等,使代码运行时可被放置任意虚拟内存- 查看编译时是否指定了
-fPIC
# 检查目标文件符号表$ nm mod1.o | grep _GLOBAL_OFFSET_TABLE_$ readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_# 如果下面等价命令任意一个有输出,表明至少一个目标模块没有-fPIC$ objdump --all-headers libfoo.so | grep TEXTREL$ readlef -d libfoo.so | grep TEXTREL复制代码
使用一个共享库
- 动态链接(动态链接本身也是个动态库ex:
/lib64/ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-2.27.so
)- 若在标准目录以外,需要设定
LD_LIBARY_PATH
gcc -g -Wall -o prog prog.c libfoo.so复制代码- 动态链接也会有静态链接的阶段,运行时,共享库程序会经历额外动态链接阶段
- 共享库别名soname, 如果设置了, 在静态连接阶段会将soname嵌入到可执行文件中(ex:将soname设为
libbar.so
)$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c -shared -W1,soname,libbar.so -o libfoo.so$ objdump -p libfoo.so | grep SONAME$ readlef -d libfoo.so | grep SONAME# 运行时需要确保有链接从soname指向真实文件$ ln -s libfoo.so libbar.so复制代码
具体流程
使用共享库的有用工具
- ldd (
ldd prog
)列出动态依赖- objdump(反汇编+readelf等等);readelf(显示ELF头部信息)
- nm显示目标库或可执行文件中定义的一组符号
共享库版本和命名规则
- 真实姓名(libdemo.so.2.0.1) libname.so.maj.min
- soname(libdemo.so.2->libdemo.so.2.0.1) libname.so.maj
- 链接器名称(libdemo.so->libdemo.so.2, 版本控制,一般连接器会指向soname)libname.so.maj
安装共享库
一些标准目录
/usr/lib
大多数标准库位置/lib
系统启动时用到的库/usr/local/lib
非标准实验性的库/etc/ld.so.conf
设定了标准库位置
ldconfig
- 解决了两个问题
- 动态连接器搜索所有文件会慢
- 若安装了新版或删除了旧版的库soname符号不是最新的会
- 维护/etc/ld.so.cache,为了构建这个缓存ldconfig会搜索在/etc/ld.so.conf里指定的目录再搜索/lib,/usr/lib,使缓存包含所有这些目录中的主要库的版本
- 检查每个库的各个主要版本的最新次要版本,找出嵌入的soname,在同一目录中为每个soname创建更新符号链接
运行时找共享库的顺序
- 可执行文件的set(DT_RPATH) - set(DT_RUNPATH)
- LD_LIBRARY_PATH 如果可执行文件是一个set-user-ID或set-group-ID程序会忽略此条(防止欺骗加载私有PATH的私有库)
- DT_RUNPATH
- /etc/ld.so.cache
- /lib,/usr/lib等
运行时的符号解析
- 默认顺序是主程序全局符号覆盖库中相应的定义,若在多个库中定义,会绑定到扫描的第一个
- 链接时加上
-Bsymbolic
指定库中对全局符号的引用应该优先绑定库中相应的定义上
静态库取代共享库(略,chroot jail使用)
linux/unix编程手册-42(共享库高级特性)
动态加载库
在需要的时候在加载一个插件,动态链接库的这些功能是通过dlopen 这组api实现的
- 在linux下使用dlopen API需要指定
-ldl
选项以便和lidbl库链接起来
#includevoid *dlopen(const char *libfilename, int flags);// 返回lib的文件句柄,或者NULL// 如果libfilename包含了/会按路径搜索,不然按运行时找共享库的顺序找const char *dlerror(void);// dlopen调用失败时调用会返回具体原因void *dlsym(void *handle, char *symbol);// 返回symbol地址// 返回NULL时,通过dlerror确定是异常还是没有int dlclose(void *handle);// 0 success, -1 error# define _GNU_SOURCEint dladdr(const void *addr, D1_info *info);// 根据dlsym返回的addr, 获得infotypedef struct{ const char *dli_fname;//包含addr的共享库路径名 void *dli_fbase;//运行时基地址?? const char *dli_sname; // 小于addr地址最近的变量名 void *dli_saddr;// 就是addr}复制代码
dlopen
- 会将libfilename的共享库加载进调用进程的虚拟地址空间,并增加该库的打开引用计数
- 同一个库文件可以多次调用
dlopen()
,但是加载进内存(物理?)的操作只会有一次,所有调用返回相同句柄值,但是每一个句柄维护一个引用计数,直到全部dlclose()
,引用为0从内存删除这个库- 会自动加载
- flag参数是一个掩码位
RTLD_LAZY
:只有代码被执行的时候才去解析库中未定义的函数符号(不包含变量)RTLD_NOW
: 在dlopen未结束前立即加载库中所有未定义符号(可以提前检查出错误,一般调试时使用,设置LD_BIND_NOW环境变量为非空字符串,会覆盖RTLD_LAZY
, 相当于RTLD_NOW
)- 其他的掩码位略:达到
dlclose()
不释放,不加载等等功能- libfilename为NULL时,
dlopen()
会返回主程序的句柄
dlsym
dlsym
的参数handle
除了dlopen
返回的句柄值以外还可以用以下伪句柄值
RTLD_DEFAULT
,从主程序开始找然后从已加载共享库找(包括RTLD_GLOBAL标记的dlopen()调用动态加载的库)
dlclose
- 引用减1,依赖递归执行
gcc -export-dynamic
加上这个才可以使主程序的符号对动态链接器可用。
控制符号可见性
- static 会将可见性限制在单个源码文件
void __attribute__((visibility("hidden"))) func (void);
会将func对共享库的所有源代码文件可见,库外不可见
链接器版本脚本
版本控制符控制符号可见性,一般在.map文件定义(ex:只有v1可见)
VER_1 { global: v1; local: *;};复制代码生成可执行文件时加上
--version-script,vis.map
- 具体的版本控制相关可查阅
_asm_
相关(有点偏)
初始化和终止函数(库加载卸载自动执行的函数)
void __attribute__((constructor) load(void)){}
void __attribute__((destructor) unload(void)){}
- 老的弃用:
_init()
;_fini()
预加载共享库(略,bug调试修改加载优先级)
监控动态链接(略,LD_DEBUG,动态链接时输出额外帮助信息)
linux/unix编程手册-43(进程间通讯简介)
IPC 工具分类
- 通信: 这些工具关注进程之间的数据交换
- 同步: 这些进程关注进程和线程之间的同步
- 信号: 可以作为同步技术和通信技术
通讯工具
上图列出的通讯工具主要在进程间交换数据(也可以线程但是一般不用,因为线程共享部分虚拟内存),可分两类
- 数据传输工具:一个进程将数据写到IPC工具中(用户内存->内核内存),另一个进程从中读取(内核内存->用户内存);读消耗数据,读写进程原子性
- 共享内存:内核通过将每个进程中的页表条目指向同一块RAM分页来实现;内存造作可能不同步
同步工具
- 信号量(和信号没啥关系)
- 文件锁
- 互斥体和条件变量
比较(略)
持久性
- 进程持久性
- 内核持久性
- 文件系统持久性
linux/unix编程手册-44(管道和FIFO)
概述
管道
- 一个管道是一个字节流,不存在消息或消息边界的概念,有顺序
- 从空管道读取会阻至至少有一个字节被写入;如果管道写入端关闭,从管道中读取进程在读完所有数据之后会看到文件结束符。
- 管道是单向的(一般)
- 多个进程写入同一管道。如果同一时刻写入量不超过PIPE_BUF字节,可确保数据不会混合
- 写入数据达到PIPE_BUF字节时,write()会在必要时阻塞直到管道中的可用空间足以原子地完成操作。
创建和使用管道
#includeint pipe(int filedes[2]);// filedes[0] 读取端文件描述符// filedes[1] 写入端文件描述符复制代码
- 因为子进程可以继承父进程的文件描述符,一般
pipe()
之后可以调用fork()
- 管道只可通过
fork()
的传递来允许相关进程间的通讯- 未关闭管道文件描述符
- 读进程未关闭写描述符可能会导致读一直阻塞
- 写进程未关闭读描述符;正常情况当
write()
一个没有读描述符的管道,内核会发送信号SIGPIPE,默认情况会杀死进程,进程可以捕获或者忽略该信号,抛出EPIPE异常;这些信号和异常时有用的,未关闭会导致无这些有用信息,导致一直写入,写满后写阻塞- 当所有进程中的引用一个管道的描述符关闭,管道才会关闭
#include// 父写子读int main(void){ int filedes[2]; if (pipe(filedes) ==-1) exit(-1); switch(fork()){ case -1: exit(-1); case 0: if (close(filedes[1])==-1) exit(-1); break; default: if (close(filedes[0])==-1) exit(-1); break; } }复制代码
-------------------下提交次分隔符-------------------
将管道作为进程同步的方法
通过pipe一些进程写,一些进程读
使用管道连接过滤器(|)
pipe()
默认会分配最小的两个文件描述符(3,4)// 绑定标准输出到管道输入 int pfd[2]; pipe(pfd); if(pfd[1] != STDOUT_FILENO){ dup2(pfd[1], STDOUT_FILENO); close(pfd[1]); }复制代码
通过管道和shell命令通信
#includeFILE *popen(const char *command, const char *mode);// mode 为r 或者wint pclose(FILE *stream)复制代码
和
system()
比较下
管道和stdio缓冲(和FILE处理一样)
FIFO
- FIFO和管道类似,但是FIFO在文件系统中有名称,打开方式和普通文件一样,所以能在非相关进程间通信
$ mkfifo [-m mode] pathname// mode 是文件权限类似chmod复制代码
#includeint mkfifo(const char *pathname, mode_t mode);复制代码
- 类似管道一般一个进程通过
O_RDONLY
打开会阻塞直到另一个通过O_WRONLY
打开,反之亦然,如果设置O_NONBLOCK
则不会阻塞- 如果open时指定了
O_RDWR
不会阻塞,但是会或导致未定义结果(避免发生)
$ mkfifo myfifo$ wc -l < myfifo < &$ ls -l | tee myfifl | sort -k5n复制代码
非阻塞I/O
O_NONBLOCK的作用
- 允许单个进程打开FIFO的两端
- 防止多个FIFO进程产生死锁
管道和FIFO中read()
和write()
的语义
write()
read()
linux/unix编程手册-45(system v ipc介绍)
SYSTEM V IPC 包括三种通信机制
- 消息队列,类似管道,但是:
- 消息队列存在边界,不是字节流
- 每条消息包含整形的type字段,可以通过type选择消息
- 信号量:是一个由内核维护的整数值,对具有相应权限的进程可见,通过值的修改进程通讯
- 共享内存:是被映射到多个虚拟内存的页帧;和信号量不同,是在用户空间的。
概述
id = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR)// 失败时id=-1, 通过key值区分//如果key值不存在,指定了IPC_CREAT会创建,否则返回ENOENT错误,//如果指定了IPC_EXCL,需要确保进程是创建IPC进程,若存在对应key的IPC,返回EEXIST错误复制代码
IPC 和 文件系统
- 文件描述符是个进程特性,IPC标识符是IPC对象本身的一个属性并且系统全局可见
- 删除一个IPC,消息队列和信号量是立即生效,共享内存和文件类似,所有使用该内存段的进程和内存段分离|所有引用文件的打开的文件描述符都被关闭,才会删除
IPC 对象内核持久性
IPC KEY
产生唯一KEY的方法
- 所有IPC程序包含一个定义了各种KEY值的头文件
xxxget
方法时id=msgget(IPC_PRIVATE)
ftok()
key_t ftok(char *pathname, int proj)
通过文件inode(不同链接指向同一文件会一致)和proj生成唯一key
关联数据结构和对象权限
- 所有IPC对象关联数据结构还包含一个
ipc_perm
的子结构struct ipc_perm{ key_t __key; // get 时传的ket,SUSv3要求除了__key, __seq外其它字段都要具备 uid_t uid; // owner user id gid_t gid; // owner group id uid_t cuid, //creator user id 不可修改 gid_t cgit; // creator group id 不可修改 unsigned short mode; // 权限,9位,和文件权限类似,但对于IPC只有读写权限有意义 unsigned short __seq;}复制代码- IPC对象上进程权限分配的规则,类似文件系统(区别在文件是文件系统用户ID(一般也等于euid)):
- 特权进程,赋予全部权限
- 进程的euid和IPC的uid或者cuid一致,会将user权限赋予进程
- egid或者任意一辅助组ID和gid或cgid一致,则将group权限赋予进程
- 赋予other权限
- 所需权限的概述
- 从IPC对象获取信息(消息队列读消息,获取一个信号量的值,读取而附上一个共享内存段)需要读权限
- 从IPC对象更新信息(消息队列写消息,修改一个信号量的值,写入而附上一个共享内存段)需要写权限
- 获取IPC对象关联数据结构的副本
IPC_STAT
操作需要读权限- 删除一个IPC对象
IPC_RMID
或者修改关联数据结构IPC_SET
操作需要使特权进程或者euid=uid 或 cuid
IPC标识符和C/S应用程序
#include#include #include #include #include #include #include #define KEY_FILE "/tmp/ipc"int main(int argc, char *argv[]){ int msqid; key_t key; const int MQ_PERMS = S_IRUSR | S_IWUSR | S_IWGRP; key = ftok(KEY_FILE, 1); if (key == -1){ exit(-1); } while((msqid = msgget(key, IPC_CREAT | IPC_EXCL | MQ_PERMS)) == -1){ if (errno == EEXIST){ msqid = msgget(key, 0); if (msqid == -1) exit(-1); if (msgctl(msqid, IPC_RMID, NULL) == -1) exit(-1); printf("Remove old msg queue (id=%d)\n", msqid); } else exit(-1); } exit(0);}复制代码
- 服务端重启时需要关闭之前打开的IPC,因为新进程不清楚之气前旧进程的状态和历史信息
- 内核确保了在创建新IPC对象时,即使传入的KEY是一样的,会的到一个不同的标识符,所有客服端使用就标识符会从相关的IPC调用得到错误
System V IPC get调用的算法
内核会为每一周IPC维护一个ipc_ids的结构(下图semid_ids是信号量的示例)
执行get调用时
- 在entries中搜索key字段
- 如果没有匹配到key且没有指定IPC_CREAT,返回ENOENT错误
- 匹配到了,但是指定了IPC_CREAT|IPC_EXCL,返回EEXIT
- 未匹配到创建,并执行以下步骤,或者匹配到了跳过以下步骤
- 如果没有找到匹配结构指定了IPC_CREAT,会分配一个对应的关联数据结构(ex:semid_ids)并初始化
- 更新ipc_ids的各个字段,指向新结构的指针会放在entries的第一个未被占用的位置
- 将key值复制到xxx_perm.__key中,seq复制到xxx_perm.seq中同时
seq+=1
- 使用以下公式计算标识符
identifier=index + xxx_perm.seq * SEQ_MULTIPLIER
- index是entries数组下标,
- SEQ_MULTIPLIER一般为32768,为
include/linux/ipc.h
中的IPCMNI(也是每种IPC数量的上限)- 当seq的值达到INT_MAX/IPCMNI(ex:2147483647/32768=65535)时,seq会重置0(极低概率发生重复)
index=identifier%SEQ_MULTIPLIER
- 在IPC调用时(ex:
msgctl
)传入了和既有对象不匹配的标识符
- 计算得到的entries[index]为空,返回EINVAL
- seq和关联数据结构seq不匹配,认为原先被删除,返回EIDRM(EX:之前客户端异常)
ipcs和ipcrm命令(略看man)
[alian@lian ~]$ ipcs------ Message Queues --------key msqid owner perms used-bytes messages ------ Shared Memory Segments --------key shmid owner perms bytes nattch status 0x6c010345 0 zabbix 600 117192 6 ------ Semaphore Arrays --------key semid owner perms nsems 0x7a010345 0 zabbix 600 12 [alian@lian ~]$ ipcs -l------ Messages Limits --------max queues system wide = 3675max size of message (bytes) = 8192default max size of queue (bytes) = 16384------ Shared Memory Limits --------max number of segments = 4096max seg size (kbytes) = 18014398509465599max total shared memory (kbytes) = 18014398442373116min seg size (bytes) = 1------ Semaphore Limits --------max number of arrays = 128max semaphores per array = 250max semaphores system wide = 32000max ops per semop call = 32semaphore max value = 32767复制代码
- ipcs只能看有读权限IPC
- /proc/sysvipc 有所有的ipc信息
$ ipcrm -X key
$ ipcrm -x id