linux/unix编程手册-41_45

Linux共享库与IPC详解
本文深入探讨了Linux下的共享库概念,包括静态库与共享库的区别、共享库的创建和使用方法,以及动态加载库的技术。此外,还介绍了Linux进程间通讯(IPC)的基本原理,包括管道、FIFO、消息队列、信号量和共享内存等工具的使用。

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.a
mod1.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.o
or
$ 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<dlfcn.h>

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_SOURCE
int dladdr(const void *addr, D1_info *info);
// 根据dlsym返回的addr, 获得info

typedef 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<unistd.h>

int pipe(int filedes[2]);
// filedes[0] 读取端文件描述符
// filedes[1] 写入端文件描述符
复制代码
  • 因为子进程可以继承父进程的文件描述符,一般pipe()之后可以调用fork()
  • 管道只可通过fork()的传递来允许相关进程间的通讯
  • 未关闭管道文件描述符
    • 读进程未关闭写描述符可能会导致读一直阻塞
    • 写进程未关闭读描述符;正常情况当write()一个没有读描述符的管道,内核会发送信号SIGPIPE,默认情况会杀死进程,进程可以捕获或者忽略该信号,抛出EPIPE异常;这些信号和异常时有用的,未关闭会导致无这些有用信息,导致一直写入,写满后写阻塞
    • 当所有进程中的引用一个管道的描述符关闭,管道才会关闭
#include<unistd.h>

// 父写子读
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<stdio.h>

FILE *popen(const char *command, const char *mode);
// mode 为r 或者w
int pclose(FILE *stream)
复制代码

system()比较下

管道和stdio缓冲(和FILE处理一样)
FIFO
  • FIFO和管道类似,但是FIFO在文件系统中有名称,打开方式和普通文件一样,所以能在非相关进程间通信
$ mkfifo [-m mode] pathname
// mode 是文件权限类似chmod
复制代码
#include<sys/stat.h>

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<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>

#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 = 3675
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384

------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 18014398509465599
max total shared memory (kbytes) = 18014398442373116
min seg size (bytes) = 1

------ Semaphore Limits --------
max number of arrays = 128
max semaphores per array = 250
max semaphores system wide = 32000
max ops per semop call = 32
semaphore max value = 32767
复制代码
  • ipcs只能看有读权限IPC
  • /proc/sysvipc 有所有的ipc信息
  • $ ipcrm -X key
  • $ ipcrm -x id
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值