博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
linux/unix编程手册-41_45
阅读量:6025 次
发布时间:2019-06-20

本文共 10588 字,大约阅读时间需要 35 分钟。


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库链接起来
#include
void *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()会在必要时阻塞直到管道中的可用空间足以原子地完成操作。

创建和使用管道

#include
int 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命令通信

#include
FILE *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复制代码
#include
int 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

转载地址:http://ryhqx.baihongyu.com/

你可能感兴趣的文章
解决https://localhost:1158/em 页面无法打开的问题
查看>>
[Cocoa]深入浅出Cocoa之Core Data(4)- 使用绑定
查看>>
原理:什么是Quadtrees?(转)
查看>>
记:返回方法参数的值(或多个值),
查看>>
Effective C++ 的52个条款列表
查看>>
c#读取ini文件
查看>>
一阶微分方程的求解
查看>>
2015年国际智慧教育展览会盛大开幕
查看>>
美国中央情报局CIA正通过开发人工智能项目,收集与检索社交媒体情报
查看>>
SanDisk闪迪推面向VMware Virtual SAN 6的增强型闪存
查看>>
Spring IoC 学习(3)
查看>>
使用TaskManager爬取2万条代理IP实现自动投票功能
查看>>
Lucene in action 笔记 analysis篇
查看>>
其它 Helper
查看>>
监控利器Prometheus初探
查看>>
foreach遍历打印表格
查看>>
Oracle笔记(中) 多表查询
查看>>
MusicXML 3.0 (7) - 连线、延音线
查看>>
Delphi 中的 XMLDocument 类详解(5) - 获取元素内容
查看>>
NIS MAP
查看>>