相关概念
程序vs进程vs命令: Linux系统上所有的操作由进程
完成,进程
的运行是动态的,在此之前是一个静态的程序
。用户用一个程序来启动一个进程,这个程序可以是别人写好的(最终被编译成可执行文件),比如ls
、pwd
、cat
,也可以是我们自己写的。
系统调用: 无论如何,程序最后运行起来都是进程,并且一个程序想要在系统上跑,要用到系统调用
,这是系统给用户提供的API接口。
trace命令: Linux有个命令strace,常用来跟踪进程执行时系统调用和所接收的信号。通过manstrace
查看具体描述。
Glibc: 作为一个开发者,也许平时并没有直接使用系统调用,而是Glibc库。Glibc是Linux下使用的开源的标准C库它是GNU发布的libc库。Glibc
即系统调用的封装。
介绍系统调用
然后本文开始介绍这些系统调用,先上图
进程管理
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
。
理解: 用户调用通过软中断进入内核态,在系统调用表中找到对应的内核系统调用,执行内核调用之前保存用户态寄存器,执行内核调用后返回并恢复用户态。
64位系统调用过程
DO_CALL定义在源码位置unix/sysv/linux/x86_64/sysdep.h
,还是将系统调用名称转换为系统调用号,放到寄存器 rax。和32位不同的是,这里是真正进行调用,不是用中断了,而是改用 syscall 指令了,而且传递参数的寄存器也变了。
理解: 用户调用通过syscall指令进入内核态,在系统调用表中找到对应的内核系统调用,执行内核调用之前保存用户态寄存器,执行内核调用后返回并恢复用户态。
整个流程
补充
系统调用表
数据结构定义在arch/x86/entry/syscall_64.c
,系统调用列表输出在arch/x86/entry/syscalls/syscall_64.tbl
#系统调用号 abi类型 函数名 系统调用名
2 common open sys_open
系统调用函数声明
声明在include/linux/syscalls.h
,找到有sys_open 的声明
系统调用函数实现
内核系统调用实现和声明一致,其中fs/open.c
编译规则
接下来,在编译的过程中,需要根据 syscall_32.tbl 和 syscall_64.tbl 生成自己的 unistd_32.h 和 unistd_64.h。在文件arch/x86/entry/syscalls/Makefile
中
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。