系统调用

只要执行系统调用或者库函数,检查返回状态以确定调用是否成功,是编程的一条铁律。

系统调用与库函数很像,其幕后会经历诸多步骤:

  1. 通过调用C语言库函数的外壳函数,发起系统调用
  2. 对系统调用中断处理例程来说,外壳函数必须保证所有的系统调用参数可用
  3. 内核需要区分每个系统调用,将系统编号复制到一个特殊的寄存器
  4. 外壳函数执行一条中断指令int 0x80,引发处理器从用户态切到核心态,并执行系统中断0x80的中断矢量所指向的代码
  5. 为了响应中断0x80,内核会调用system_call来处理这次中断
    • 内核栈中保存寄存器
    • 审核系统调用编号的有效性
    • 以系统调用编号存放所有调用服务的列表(内核变量sys_call_table)进行索引,发现并调用相应的系统服务
    • 从内核栈中恢复各个寄存器,将系统调用返回值放在栈中
    • 返回外壳函数,同时将处理器切回用户态
  6. 若系统调用例程的返回值表明调用有误,外壳函数使用该值设置全局变量errno,然后外壳函数返回到调用程序,同时返回一个整型值,表明系统调用是否成功

一个简单的系统调用,也要完成相当多的工作,因此系统调用开销虽小,却不容忽视

库函数

许多库函数不会使用任何系统调用,有些库函数构建于系统调用层之上,比如fopen利用open来执行打开文件的实际操作,又如printf相对于write,设计库函数为了提供比底层更为方便的调用接口

标准C语言函数库:glibc

确定系统的glibc版本:

  1. /lib/libc.so.6

发现glic共享库的路径:

  1. ldd prog | grep libc

处理系统调用和库函数的错误

不检查状态值,少敲几个字,听起来很诱人,却得不偿失,除了少数几个系统调用从不失败,如getpid总能返回进程id,_exit总能终止进程,绝大多数系统调用和库函数都应该检查返回值以确认是否调用成功

  • 系统调用失败时,会将errno设置为一个正值,以标识具体的错误
  • 进行错误检查时,应该首先检查函数的返回值是否调用出错,再检查errno确定错误原因
  • 少数系统调用即使成功,也返回-1。如getpriority,要判断此类系统调用,应在调用前将errno设置为0,并在调用后对其检查
  • perror会打印msg所指向的字符串,紧跟一条与当前errno值相对应的消息 ```c

    include

    void perror(const char *msg);

if (-1 == (fd = (open(pathname, flags, mode)))) { perror(“open”); exit(EXIT_FAILURE); }

  1. - **strerror**会针对errnum指定的错误号,返回相应的字符串
  2. ```c
  3. #include <string.h>
  4. char *strerror(int errnum);

处理库函数的错误:

  • 某些库函数返回错误信息的方式与系统调用完全相同,返回值为-1,以errno标识具体错误,如remove
  • 某些库函数出错会返回-1之外的其他值,仍会设置errno标识具体错误,如fopen,此类可用perror和stderr诊断
  • 还有些库函数根本不使用errno,此类不可用perror和stderr诊断

    可移植性问题

    系统调用和库函数的受限,被各种标准制约,一部分是SUS制定,一部分是BSD和System V Release 4定义

    特性测试宏

  • _POSIX_SOURCE:定义后会符合POSIX.1-1990和ISO C-1990标准的定义,已经被 _POSX_C_SOURCE取代

  • _POSX_C_SOURCE:同 _POSIX_SOURCE
  • _XOPEN_SOURCE:定义后,会符合POSIX.1、POSIX.2和XPG4标准
  • _BSD_SOURCE:开启BSD定义的支持
  • _SVID_SOURCE:符合System V接口规范
  • _GNU_SOURCE,定义后,会符合前述所有标准的定义,同时开启对各种GNU扩展定义的支持

    系统数据类型

    系统数据类型大多定义在,少量在其他文件,使用这些变量,才能保证可移植性
    1. caddr_t 核心地址
    2. clock_t 时钟滴答计数器(进程时间)
    3. comp_t 压缩的时钟滴答
    4. dev_t 设备号(主和次)
    5. fd_set 文件描述符集
    6. fpos_t 文件位置
    7. gid_t 数值组ID
    8. ino_t i节点编号
    9. mode_t 文件类型,文件创建模式
    10. nlink_t 目录项的链接计数
    11. off_t 文件大小和偏移量(带符号的)
    12. pid_t 进程ID和进程组ID(带符号的)
    13. ptrdiff_t 两个指针相减的结果(带符号的)
    14. rlim_t 资源限制
    15. sig_atomic_t 能原子地访问的数据类型
    16. sigset_t 信号集
    17. size_t 对象(例如字符串)大小(不带符号的)
    18. ssize_t 返回字节数的函数(带符号的)(readwrite
    19. time_t 日历时间的秒计数器
    20. uid_t 数值用户ID
    21. wchar_t 能表示所有不同的字符码
    C99为printf定义了名字为z的长度修饰符,表明紧随其后的整型转换是与size_t或ssize_t类型相对应,可以使用%zd来取代%ld外加类型转换了

    初始化操作与使用结构

    SUSv3虽然定义了诸如sembuf之类的类的结构,但是未对字段顺序做出规范,且某些实现会增加额外字段

    使用并非所有实现都定义的宏

    并非所有的UNIX实现都对某个宏做了定义,如WCOREDUMP,用于检测子进程是否生成了coredump核心转储文件,应用非常广泛,某些UNIX实现可能并没有定义,因为SUSv3未对其进行规范
    #ifdef WCOREDUMP
      //
    #endif
    

不同实现所需头文件的变化

有些情况下,包含各种系统实现和库函数原型的头文件,在不同实现之间会有所不同