天天看点

Linux系统调用

特此说明: 刘超的趣谈linux操作系统是比较重要的参考资料,本文大部分内容和所有图片来源于这个专栏。

相关概念

程序vs进程vs命令: Linux系统上所有的操作由

进程

完成,

进程

的运行是动态的,在此之前是一个静态的

程序

。用户用一个程序来启动一个进程,这个程序可以是别人写好的(最终被编译成可执行文件),比如

ls

pwd

cat

,也可以是我们自己写的。

系统调用: 无论如何,程序最后运行起来都是进程,并且一个程序想要在系统上跑,要用到

系统调用

,这是系统给用户提供的API接口。

trace命令: Linux有个命令strace,常用来跟踪进程执行时系统调用和所接收的信号。通过

manstrace

查看具体描述。

Glibc: 作为一个开发者,也许平时并没有直接使用系统调用,而是Glibc库。Glibc是Linux下使用的开源的标准C库它是GNU发布的libc库。

Glibc

即系统调用的封装。

介绍系统调用

然后本文开始介绍这些系统调用,先上图

Linux系统调用
进程管理

linux操作系统使用

fork

从父进程中创建子进程,子进程

execve

运行程序(二进制文件),父进程

waitpid

等待子进程结束。

所有进程都是父进程

fork

出来的,对于操作系统而言第一个鼻祖进程是哪来的呢? 系统启动的时候先创建一个所有用户进程的“祖宗进程”。

内存管理

在操作系统中,每个进程都有自己的

进程内存空间

。其中布局就有

代码段

数据段

一个进程的内存空间是很大的,32位的是4G,64位的就更大了。物理空间是有限的,所以进程的空间不能事先分配好的,一定是需要的时候再分配。

brk

mmap

是官员堆分配内存的系统调用,分配内存数量比较小的时候,使用

brk

会和原来的堆的数据连在一起。当分配的内存数量比较大的时候,使用mmap,会重新划分一块区域。

文件管理

文件系统相当于公司的资料库,用于保存一些永久性质的数据。能做到长期保存,文件之所以能做到这一点,一方面是因为

介质

,另一方面是因为

格式

对于文件的操作,无非是

创建creat

打开open

读read

写write

等等

Linux中一切皆文件

就包括

二进制文件

文本文件

stdout文件

Socket文件

设备文件

目录文件

,包括进程运行起来在

/proc

下生成的进程号也是文件。对于每一个文件,Linux分配了

文件描述符

,这是一个整数。

信号处理

信号是异步处理机制,用于紧急突发情况。每一种信号都有默认动作,当然用户也能编写信号处理函数,通过

sigaction

系统调用进行处理。

进程间通信

本地进程之间实现数据的互通,比较常见的处理机制有

消息队列

共享内存

通过

msgget

创建一个新的队列,

msgsnd

将消息发送到消息队列,而消息接收方可以使用

msgrcv

从队列中取消息; 我们可以通过

shmget

创建一个共享内存块,通过

shmat

将共享内存映射到自己的内存空间,然后就可以读写了。

网络通信

内核中有TCP/IP网络协议栈的实现,可以通过socket来实现跨系统的进程间通信。

查看源码

下载内核源码,找到

./include/asm-x86_64/unistd.h

文件,里面对于系统调用的定义

hinzer@ubuntu:linux-2.6.11$ head ./include/asm-x86_64/unistd.h
#ifndef _ASM_X86_64_UNISTD_H_
#define _ASM_X86_64_UNISTD_H_

#ifndef __SYSCALL
#define __SYSCALL(a,b) 
#endif

/*
 * This file contains the system call numbers.
 *
           

查看过程

进程通过系统调用从用户态到内核态,

用户态 - 系统调用 - 保存寄存器 - 内核态执行系统调用 - 恢复寄存器 - 返回用户态

。对于应用开发,上层还通过

glibc

库(对系统调用的一个封装库)。

从glibc提供的open函数出发,剖析如何从glibc的

open

调用到内核的

sys_open

!!!
glibc封装
# 以下相关文件
./sysdeps/unix/syscalls.list   # 列出所有glibc的函数对应的系统调用
./sysdeps/unix/make-syscalls.sh # 根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件
./sysdeps/unix/syscall-template.S  # 定义了这个系统调用的调用方式
./sysdeps/hppa/sysdep.h   # 通过 `vim -t PSEUDO` 找到 PSEUDO 这个宏的定义。
           

经过分析: open函数的代码逻辑,得出结论: 对于任何的系统调用,会调用

DO_CALL

。这也是一个宏,这个宏 32 位和 64 位的定义是不一样的。

32位系统调用过程

继续分析glibc源码,发现宏

DO_CALL

定义处

unix/sysv/linux/i386/sysdep.h

理解: 用户调用通过软中断进入内核态,在系统调用表中找到对应的内核系统调用,执行内核调用之前保存用户态寄存器,执行内核调用后返回并恢复用户态。

Linux系统调用
64位系统调用过程

DO_CALL定义在源码位置

unix/sysv/linux/x86_64/sysdep.h

,还是将系统调用名称转换为系统调用号,放到寄存器 rax。和32位不同的是,这里是真正进行调用,不是用中断了,而是改用 syscall 指令了,而且传递参数的寄存器也变了。

理解: 用户调用通过syscall指令进入内核态,在系统调用表中找到对应的内核系统调用,执行内核调用之前保存用户态寄存器,执行内核调用后返回并恢复用户态。

Linux系统调用
整个流程
Linux系统调用

补充

系统调用表

数据结构定义在

arch/x86/entry/syscall_64.c

,系统调用列表输出在

arch/x86/entry/syscalls/syscall_64.tbl

#系统调用号 abi类型 函数名                      系统调用名
2       common  open                    sys_open
           
系统调用函数声明
系统调用函数实现
编译规则

参考资料

  • 趣谈Linux操作系统 - 05系统调用
  • 趣谈Linux系统专栏 - 09系统调用
  • 如何下载查看glibc源代码
  • glibc源码分析(一)系统调用
  • linux下实现一个系统调用

继续阅读