入门
Why?
- __服务端应用__
- 云计算、虚拟化、容器、大数据、人工智能,几乎都是基于 Linux 技术的。那些牛得不行的系统,团购、电商、打车、快递,都是部署在服务端,也几乎都是基于 Linux 技术的
- __代码实现__
- Linux 最大的优点就是开源。作为程序员,有了代码,啥都好办了。只要有足够的耐心,我们就可以一层一层看下去,看内核调度函数,看内存分配过程。理论理解起来不容易,但是一行行的“if-else”却不会产生歧义
- 当你写代码的时候,大部分情况下都可以使用现成的数据结构和算法库,但是有些场景对于内存的使用需要限制到很小,对于搜索的时间需要限制到很小的时候,我们需要定制化一些数据结构,这个时候内核里面这些实现就很有参考意义了
- __系统生态__
- 假设,我们现在就是在做一家外包公司,我们的目标是把这家公司做上市。其中,操作系统就是这家外包公司的老板。我们把这家公司的发展阶段分为这样几个时期:
- **初创期**:这个老板基于开放的营商环境(x86体系结构),创办一家外包公司(系统的启动)。因为一开始没有其他员工,老板需要亲自接项目(实模式)。'
- '**发展期**:公司慢慢做大,项目越接越多(保护模式、多进程),为了管理各个外包项目,建立了项目管理体系(进程管理)、会议室管理体系(内存管理)、文档资料管理系统(文件系统)、售前售后体系(输入输出设备管理)。
- **壮大期**:公司越来越牛,开始促进内部项目的合作(进程间通信)和外部公司合作(网络通信)。
- **集团化**:公司的业务越来越多,会成立多家子公司(虚拟化),或者鼓励内部创业(容器化),这个时候公司就变成了集团。大管家的调度能力不再局限于一家公司,而是集团公司(Linux集群),从而成功上市(从单机操作系统到数据中心操作系统)。
Test
Route
- Linux 的灵活性也会使得你有 N 多种方法解决问题,从而事半功倍,你就会有一切尽在掌握的感觉
- [[commandline]]
- 在这样没有统一入口的情况下,就需要你对最基本的命令有所掌握
- ==这个过程可能非常痛苦,在没有足够熟练地掌握命令行之前,你会发现干个非常小的事情都需要搜索半天,读很多文档,即便如此还不一定能得到期望的结果==。这个时候你一定不要气馁,坚持下去,继续看文档、查资料.
- `When Done`
- 慢慢你就会发现,大部分命令的行为模式都很像,你几乎不需要搜索就能完成大部分操作了
- **鸟哥的 Linux 私房菜** / **Linux 系统管理技术手册**
- __程序设计__ <- 系统调用 / glibc
- __如果说使用命令行的人是吃馒头的,那写代码操作命令行的人就是做馒头的__
- Linux 的系统调用非常多,而且每个函数都非常复杂,传入的参数、返回值、调用的方式等等都有很多讲究。这里面需要掌握很多 Linux 操作系统的原理,否则你会无法理解为什么应该这样调用。你会发现,你平时用的一个简单的命令行,却需要 N 个系统调用组合才能完成。其中每个系统调用都要进行深入地学习、读文档、做实验。
- `When Done`
- 大学里学的那些理论,你再回去看,现在就会开始有感觉了
- **UNIX 环境高级编程**
- __Linux 内核机制__
- 你的角色要再次面临变化, **就像你蒸馒头时间长了,发现要蒸出更好吃的馒头,就必须要对面粉有所研究**.
- 我不建议你直接看代码,因为 Linux 代码量太大,很容易迷失,找不到头绪。最好的办法是, __先了解一下 Linux 内核机制,知道基本的原理和流程就可以了__
- `When Done`
- 你会发现 Linux 这个复杂的系统开始透明起来。无论你是运维,还是开发,你都能大概知道背后发生的事情,并在出现异常的情况时,比较准确地定位到问题所在
- **深入理解 LINUX 内核**
- __Linux 内核代码__
- 理论的描述和提炼虽然能够让你更容易看清全貌,但是容易让你忽略细节
- **一开始阅读代码不要纠结一城一池的得失,不要每一行都一定要搞清楚它是干嘛的,而要聚焦于核心逻辑和使用场景**
- `When Done`
- 对于操作系统的原理,你应该就掌握得比较清楚了。就像蒸馒头的人已经将面粉加工流程烂熟于心。这个时候, 你就 __可以有针对性地去做课题,把所学和你现在做的东西结合起来重点突破__
- 例如你是研究虚拟化的,就重点看 KVM 的部分; 如果你是研究网络的,就重点看内核协议栈的部分
- **LINUX 内核源代码情景分析**
- __Linux 定制组件__
- 因为 Linux 有源代码,很多地方可以参考现有的实现,定制化自己的模块
- 这个难度比较大,涉及的细节比较多,上一个阶段,我的建议是不计较一城一地的得失,不需要每个细节都搞清楚,这一个阶段要求就更高了
- __真实场景开发__
- 生产环境会有大量的不可控因素,尤其是集群规模大的更是如此,大量的运维经验是实战来的,不能光靠读书。
- 如果你是开发,对内核进行少量修改容易,但是一旦面临真实的场景,需要考虑各种因素, __并发与并行,锁与保护,扩展性和兼容性,__ 都需要真实项目才能练出来
综述
Subsystem
- 
- 
- [[ls]]
- Install
- 在 Windows 里面,最终会变成 C:\Program Files 下面的一个文件夹以及注册表里面的一些配置。对应 Linux 里面会放的更散一点。例如,主执行文件会放在 /usr/bin 或者 /usr/sbin 下面,其他的库文件会放在 /var 下面,配置文件会放在 /etc 下面
- [[nohup]]
系统调用
- [[linux-system-call]]
- > "一切皆文件"
- 启动一个进程,需要一个程序文件,这是一个**二进制文件**。
- 启动的时候,要加载一些配置文件,例如 yml、properties 等,这是文本文件;启动之后会打印一些日志,如果写到硬盘上,也是**文本文件**。
- 但是如果我想把日志打印到交互控制台上,在命令行上唰唰地打印出来,这其实也是一个文件,是标准输出**stdout 文件**。
- 这个进程的输出可以作为另一个进程的输入,这种方式称为**管道**,管道也是一个文件。
- 进程可以通过网络和其他进程进行通信,建立的**Socket**,也是一个文件。
- 进程需要访问外部设备,**设备**也是一个文件。
- 文件都被存储在文件夹里面,其实**文件夹**也是一个文件。
- 进程运行起来,要想看到进程运行的情况,会在 /proc 下面有对应的**进程号**,还是一系列文件。
- 每个文件,Linux 都会分配一个**文件描述符**(File Descriptor),这是一个整数。有了这个文件描述符,我们就可以使用系统调用,查看或者干预进程运行的方方面面。
系统初始化
x86
- IBM 和 x86 的故事
- IBM 开始做 IBM PC 时,一开始并没有让最牛的华生实验室去研发,而是交给另一个团队。一年时间,软硬件全部自研根本不可能完成,于是他们采用了英特尔的 8088 芯片作为 CPU,使用微软的 MS-DOS 做操作系统。
- 谁能想到 IBM PC 卖的超级好,好到因为垄断市场而被起诉。IBM 就在被逼的情况下公开了一些技术,使得后来无数 IBM-PC 兼容机公司的出现,也就有了后来占据市场的惠普、康柏、戴尔等等。
- 能够开放自己的技术是一件了不起的事。从技术和发展的层面来讲,它会使得一项技术大面积铺开,形成行业标准。就比如现在常用的 Android 手机,如果没有开放的 Android 系统,我们也没办法享受到这么多不同类型的手机。
- 对于当年的 PC 机来说,其实也是这样。英特尔的技术因此成为了行业的开放事实标准。由于这个系列开端于 8086,因此称为 x86 架构
- 计算机的工作模式
- 
进程管理
ch01 进程
- `build-essential` in [[ubuntu]]
$ apt show build-essential
...
Depends: libc6-dev | libc-dev, gcc (>= 4:9.2 ), g++ ( > = 4:9.2), make, dpkg-dev ( > = 1.17.11)
...
Compiler
//process.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
extern int create_process ( char* program , char** arg_list );
int create_process ( char* program , char** arg_list )
{
pid_t child_pid;
child_pid = fork ();
if (child_pid != 0 )
return child_pid;
else {
execvp (program, arg_list);
abort ();
}
}
// createprocess.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
extern int create_process ( char* program , char** arg_list );
int main ()
{
char* arg_list[] = {
"ls" ,
"-l" ,
"/etc/yum.repos.d/" ,
NULL
};
create_process ( "ls" , arg_list);
return 0 ;
}
- 
- ELF
- 可重定位文件 (Relocatable File) `.o`

- 可重定位:
- 文件的头 用于描述整个文件
- 文件格式在内核中有定义,分别为 `struct elf32_hdr` 和 `struct elf64_hdr`
- `.text`:放编译好的二进制可执行代码
- `.data`:已经初始化好的全局变量
- `.rodata`:只读数据,例如字符串常量、const 的变量
- `.bss`:未初始化全局变量,运行时会置 0
- `.symtab`:符号表,记录的则是函数和变量
- `.strtab`:字符串表、字符串常量和变量名
- 局部变量是放在栈里面, 在程序运行过程中随时分配空间, 随时释放的
- 编译的时候先做预处理工作, 如将头文件嵌入到正文中,将定义的宏展开,然后编译过程,-> `.o` 文件 (ELF 第一种类型)
- ELF made by section(节)
- 可执行文件

- 这些 `section` 是多个`.o` 文件合并过的
- 将小的 `section` 合成了大的段 `segment`,并且在最前面加一个段头表 (Segment Header Table)
- 在代码里面的定义为 `struct elf32_phdr` 和 `struct elf64_phdr`,这里面除了有对于段的描述之外,最重要的是 p_vaddr,这个是这个段加载到内存的虚拟地址。在 ELF 头里面,有一项 e_entry,也是个虚拟地址,是这个程序运行的入口。
- 共享对象文件 (Shared Object) ref -> ((6253d4ec-85ba-4879-b2db-71ba04b8b9b8))
- diff
- 一个`.interp` 的 Segment
- 这里面是 `ld-linux.so`,动态链接器, 运行时的链接动作都是它做的
- 两个 `section`
- `.plt`: 过程链接表 (Procedure Linkage Table, PLT)
- `.got.plt`: 全局偏移量表 (Global Offset Table,GOT)
- `dynamiccreateprocess` 这个程序要调用 `libdynamicprocess.so` 里的 `create_process` 函数
- 编译 -> 在 `PLT` 里面建立一项 `PLT[x]` -> 二进制中直接调用 `PLT[x]` 里面的代理代码 -> 代理代码在运行时到 `GOT` (`GOT[y]`: 运行时 `create_process` 函数在内存中真正的地址) 找真正的 `create_process` 函数
- `GOT[y]` -> 回调 `PLT` -> 调用 `PLT[0]` -> 调用 `GOT[2]`( `ld-linux.so` 的入口函数) -> 拿到 `create_process ` 函数地址, 下次,PLT[x] 的代理函数就能够直接调用了
- alalysis
- `readelf`: 分析 ELF 的信息
- `objdump`: 显示二进制文件的信息
- `hexdump`: 查看文件的十六进制编码
- `nm`: 关于指定文件中符号的信息
Link
- 静态链接库
ar cr libstaticprocess.a process.o
#archives, 将一系列对象文件(.o)归档为一个文件
gcc -o staticcreateprocess createprocess.o -L. -lstaticprocess
# -L 表示在当前目录下找.a 文件
# -lstaticprocess 会自动补全文件名, 如加前缀 lib,后缀.a,变成 libstaticprocess.a
# 找到这个.a 文件后,将里面的 process.o 取出来,和 createprocess.o 做一个链接
# 形成二进制执行文件 staticcreateprocess
- 动态链接库 (Shared Libraries)
gcc -shared -fPIC -o libdynamicprocess.so process.o
gcc -o dynamiccreateprocess createprocess.o -L. -ldynamicprocess
- 当运行这个程序的时候,首先寻找动态链接库,然后加载它。默认情况下,系统在 /lib 和 /usr/lib 文件夹下寻找动态链接库。如果找不到就会报错,我们可以设定 `export LD_LIBRARY_PATH=.` 环境变量,程序运行时会在此环境变量指定的文件夹下寻找动态链接库。
- [[exec]]
ch02 线程
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#mark: NUM_OF_TASKS 5
void * downloadfile ( void * filename ){
printf ( "I am downloading the file %s ! \n " , ( char * )filename);
sleep ( 10 );
long downloadtime = rand () % 100 ;
printf ( "I finish downloading the file within %d minutes! \n " , downloadtime);
pthread_exit (( void * )downloadtime);
//当一个线程退出的时候,就会发送信号给其他所有同进程的线程
}
int main ( int argc , char * argv []){
char files[NUM_OF_TASKS][ 20 ] = { "file1.avi" , "file2.rmvb" , "file3.mp4" , "file4.wmv" , "file5.flv" };
pthread_t threads[NUM_OF_TASKS];
int rc;
int t;
int downloadtime;
pthread_attr_t thread_attr;
pthread_attr_init ( & thread_attr);
pthread_attr_setdetachstate ( & thread_attr,PTHREAD_CREATE_JOINABLE);
// The detach state attribute determines whether a thread created using the thread
// attributes object attr will be created in a joinable or a detached state.
for (t = 0 ;t < NUM_OF_TASKS;t ++ ){
printf ( "creating thread %d , please help me to download %s\n " , t, files[t]);
rc = pthread_create ( & threads[t], & thread_attr, downloadfile, ( void * )files[t]);
//[1] 参数是线程对象
//[2] 线程属性
//[3] 线程运行函数
//[4] 线程运行函数的参数
if (rc){
printf ( "ERROR; return code from pthread_create() is %d\n " , rc);
exit ( - 1 );
}
}
pthread_attr_destroy ( & thread_attr);
for (t = 0 ;t < NUM_OF_TASKS;t ++ ){
pthread_join (threads[t],( void** ) & downloadtime);
// 获取返回值并传给主线程
printf ( "Thread %d downloads the file %s in %d minutes. \n " ,t,files[t],downloadtime);
}
pthread_exit ( NULL );
}
- 
gcc download.c -lpthread
Data
- 
- 线程栈上的本地数据
- 管理栈 `ulimit [-a][-s]`
- 修改 `int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);`
- 线程栈之间还会有小块区域,用来隔离保护各自的栈空间。一旦另一个线程踏入到这个隔离区,就会引发段错误
- 在整个进程里共享的全局数据
- 线程私有数据 (Thread Specific Data)
- 声明 `int pthread_setspecific(pthread_key_t key, const void *value)`
- 创建一个 key,伴随着一个析构函数
- 设置 key 对应的 value
- `int pthread_setspecific(pthread_key_t key, const void *value)`
- 获取 key 对应的 value
- `void *pthread_getspecific(pthread_key_t key)`
- 而等到线程退出的时候,就会调用析构函数释放 value
Protected
- Mutex (Mutual Exclusion), 互斥
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#mark: NUM_OF_TASKS 5
int money_of_tom = 100 ;
int money_of_jerry = 100 ;
// 第一次运行去掉下面这行
pthread_mutex_t g_money_lock;
void * transfer ( void * notused ){
pthread_t tid = pthread_self ();
printf ( "Thread %u is transfering money! \n " , ( unsigned int )tid);
// 第一次运行去掉下面这行
pthread_mutex_lock ( & g_money_lock);
sleep ( rand () % 10 );
money_of_tom += 10 ;
sleep ( rand () % 10 );
money_of_jerry -= 10 ;
// 第一次运行去掉下面这行
pthread_mutex_unlock ( & g_money_lock);
printf ( "Thread %u finish transfering money! \n " , ( unsigned int )tid);
pthread_exit (( void * ) 0 );
}
int main ( int argc , char * argv []){
pthread_t threads[NUM_OF_TASKS];
int rc;
int t;
// 第一次运行去掉下面这行
pthread_mutex_init ( & g_money_lock, NULL );
for (t = 0 ;t < NUM_OF_TASKS;t ++ ){
rc = pthread_create ( & threads[t], NULL , transfer, NULL );
if (rc){
printf ( "ERROR; return code from pthread_create() is %d\n " , rc);
exit ( - 1 );
}
}
for (t = 0 ;t < 100 ;t ++ ){
// 第一次运行去掉下面这行
pthread_mutex_lock ( & g_money_lock);
printf ( "money_of_tom + money_of_jerry = %d\n " , money_of_tom + money_of_jerry);
// 第一次运行去掉下面这行
pthread_mutex_unlock ( & g_money_lock);
}
// 第一次运行去掉下面这行
pthread_mutex_destroy ( & g_money_lock);
pthread_exit ( NULL );
}
- 
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#mark: NUM_OF_TASKS 3
#mark: MAX_TASK_QUEUE 11
char tasklist[MAX_TASK_QUEUE] = "ABCDEFGHIJ" ;
int head = 0 ;
int tail = 0 ;
int quit = 0 ;
pthread_mutex_t g_task_lock;
pthread_cond_t g_task_cv;
void * coder ( void * notused ){
pthread_t tid = pthread_self ();
while ( ! quit){
pthread_mutex_lock ( & g_task_lock);
while (tail == head){
if (quit){
pthread_mutex_unlock ( & g_task_lock);
pthread_exit (( void * ) 0 );
}
printf ( "No task now! Thread %u is waiting! \n " , ( unsigned int )tid);
pthread_cond_wait ( & g_task_cv, & g_task_lock);
printf ( "Have task now! Thread %u is grabing the task ! \n " , ( unsigned int )tid);
}
char task = tasklist[head ++ ];
pthread_mutex_unlock ( & g_task_lock);
printf ( "Thread %u has a task %c now! \n " , ( unsigned int )tid, task);
sleep ( 5 );
printf ( "Thread %u finish the task %c ! \n " , ( unsigned int )tid, task);
}
pthread_exit (( void * ) 0 );
}
int main ( int argc , char * argv []){
pthread_t threads[NUM_OF_TASKS];
int rc;
int t;
pthread_mutex_init ( & g_task_lock, NULL );
pthread_cond_init ( & g_task_cv, NULL );
for (t = 0 ;t < NUM_OF_TASKS;t ++ ){
rc = pthread_create ( & threads[t], NULL , coder, NULL );
if (rc){
printf ( "ERROR; return code from pthread_create() is %d\n " , rc);
exit ( - 1 );
}
}
sleep ( 5 );
for (t = 1 ;t <= 4 ;t ++ ){
pthread_mutex_lock ( & g_task_lock);
tail += t;
printf ( "I am Boss, I assigned %d tasks, I notify all coders! \n " , t);
pthread_cond_broadcast ( & g_task_cv);
pthread_mutex_unlock ( & g_task_lock);
sleep ( 20 );
}
pthread_mutex_lock ( & g_task_lock);
quit = 1 ;
pthread_cond_broadcast ( & g_task_cv);
pthread_mutex_unlock ( & g_task_lock);
pthread_mutex_destroy ( & g_task_lock);
pthread_cond_destroy ( & g_task_cv);
pthread_exit ( NULL );
}
- 
- 
进程链表
struct list_head tasks;
via: https://github.com/torvalds/linux/blob/a19944809fe9942e6a96292490717904d0690c21/include/linux/sched.h#L854=
- 
任务 ID
pid_t pid; // process id
pid_t tgid; // thread group ID
struct task_struct * group_leader;
via: https://github.com/torvalds/linux/blob/master/include/linux/sched.h & https://docs.huihoo.com/doxygen/linux/kernel/3.7/include_2linux_2sched_8h_source.html
- 只有主线程
- pid, tgid, group_leader 全指向自己
- 有其他线程
- 线程有自己的 pid,tgid 就是进程的主线程的 pid,group_leader 指向的就是进程的主线程
信号处理
/* Signal handlers: */
struct signal_struct * signal;
struct sighand_struct * sighand; // 正在通过信号处理函数进行处理
sigset_t blocked; // 被阻塞暂不处理
sigset_t real_blocked;
sigset_t saved_sigmask;
struct sigpending pending; // 尚等待处理
unsigned long sas_ss_sp; // 开辟新的栈专门用于信号处理
size_t sas_ss_size;
unsigned int sas_ss_flags;
任务状态
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
int exit_state;
unsigned int flags;
- state(状态)可以取的值定义在 include/linux/sched.h 头文件中
// bitset set
/* Used in tsk->state: */
#mark: TASK_RUNNING 0
#mark: TASK_INTERRUPTIBLE 1
#mark: TASK_UNINTERRUPTIBLE 2
#mark: __TASK_STOPPED 4
#mark: __TASK_TRACED 8
/* Used in tsk->exit_state: */
#mark: EXIT_DEAD 16
#mark: EXIT_ZOMBIE 32
#mark: EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#mark: TASK_DEAD 64
#mark: TASK_WAKEKILL 128
#mark: TASK_WAKING 256
#mark: TASK_PARKED 512
#mark: TASK_NOLOAD 1024
#mark: TASK_NEW 2048
#mark: TASK_STATE_MAX 4096
- 
- `TASK_RUNNING`: 进程在时刻准备运行的状态
- 在运行中的进程,一旦要进行一些 I/O 操作,需要等待 I/O 完毕,这个时候会释放 CPU,进入睡眠状态
- 睡眠状态
- `TASK_INTERRUPTIBLE`,可中断的睡眠状态
- 信号来被唤醒 -> 唤醒后不继续刚才的操作,而是进行信号处理
- `TASK_UNINTERRUPTIBLE`,不可中断的睡眠状态
- 死等 I/O 操作完成
- 重启电脑
- `TASK_KILLABLE`,可以终止的新睡眠状态
- 运行原理类似 `TASK_UNINTERRUPTIBLE`
- 但可以响应致命信号
- `TASK_WAKEKILL` 接收到致命信号时唤醒进程
#mark: TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
- TASK_STOPPED
- 在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态
- TASK_TRACED
- 进程被 debugger 等进程监视,进程执行被调试程序所停止
- 当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。
- 一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候它的父进程还没有使用 wait() 等系统调用来获知它的终止信息,此时进程就成了僵尸进程
- EXIT_DEAD 是进程的最终状态
- EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state
- 上面的进程状态和进程的运行、调度有关系,
Flags 标志
- 在 flags 字段中,被定义称为宏,以 PF 开头。
#mark: PF_EXITING 0x 00000004
#mark: PF_VCPU 0x 00000010
#mark: PF_FORKNOEXEC 0x 00000040
- PF_EXITING
- 正在退出
- 当有这个 flag 的时候,在函数 find_alive_thread 中,找活着的线程,遇到有这个 flag 的,就直接跳过
- PF_VCPU
- 进程运行在虚拟 CPU 上
- 在函数 account_system_time 中,统计进程的系统运行时间,如果有这个 flag,就调用 account_guest_time,按照客户机的时间进行统计
- PF_FORKNOEXEC
- fork 完了,还没有 exec
- 在 _do_fork 函数里面调用 copy_process,这个时候把 flag 设置为 PF_FORKNOEXEC。当 exec 中调用了 load_elf_binary 的时候,又把这个 flag 去掉
进程调度
- 状态切换
// 是否在运行队列上
int on_rq;
// 优先级
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
// 调度器类
const struct sched_class * sched_class;
// 调度实体
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
// 调度策略
unsigned int policy;
// 可以使用哪些 CPU
int nr_cpus_allowed;
cpumask_t cpus_allowed;
struct sched_info sched_info;
运行统计信息
u64 utime; // 用户态消耗的 CPU 时间
u64 stime; // 内核态消耗的 CPU 时间
unsigned long nvcsw; // 自愿 (voluntary) 上下文切换计数
unsigned long nivcsw; // 非自愿 (involuntary) 上下文切换计数
u64 start_time; // 进程启动时间,不包含睡眠时间
u64 real_start_time; // 进程启动时间,包含睡眠时间
进程亲缘关系
- 
struct task_struct __rcu * real_parent; /* real parent process */
struct task_struct __rcu * parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
- parent 指向其父进程。当它终止时,必须向它的父进程发送信号。
- children 表示链表的头部。链表中的所有元素都是它的子进程。
- sibling 用于把当前进程插入到兄弟链表中。
- 通常情况下,real_parent 和 parent 是一样的
- 当子进程作为父进程时候, real_parent 和 parent 不一样
进程权限
- 项目组权限的控制范畴
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu * real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu * cred;
- `Objective` -> “谁能操作我”,很显然,这个时候我就是被操作的对象
- `Subjective` -> “我能操作谁”,这个时候我就是 Subjective,那个要被我操作的就是 Objectvie
- `real_cred` 就是说明谁能操作我这个进程
- `cred` 就是说明我这个进程能够操作谁
struct cred {
......
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
......
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
......
} __randomize_layout;
- `uid` 和 `gid`
- 谁启动的进程,就是谁的 ID
- 权限审核的时候,往往不比较这两个,也就是说不大起作用
- `euid` 和 `egid`,effective user/group id
- 当这个进程要操作消息队列、共享内存、信号量等对象的时候,其实就是在比较这个用户和组是否有权限
- `suid` 和 `fsgid`,filesystem user/group id
- 对文件操作会审核的权限
- 
- `chmod u+s program` -> 设置 `set-user-ID` 的标识位,把游戏的权限变成 `rwsr-xr-x`
- `uid` 当然还是用户 A; `euid` 和 `fsuid` 都改成用户 B
- ==一个进程可以随时通过 setuid 设置用户 ID==
- 游戏程序的用户 B 的 ID 还会保存在一个地方,这就是 suid 和 sgid,也就是 saved uid 和 save gid
- 这样就可以很方便地使用 setuid,通过设置 uid 或者 suid 来改变权限。
- `capabilities` 位图
#mark: CAP_CHOWN 0
#mark: CAP_KILL 5
#mark: CAP_NET_BIND_SERVICE 10
#mark: CAP_NET_RAW 13
#mark: CAP_SYS_MODULE 16
#mark: CAP_SYS_RAWIO 17
#mark: CAP_SYS_BOOT 22
#mark: CAP_SYS_TIME 25
#mark: CAP_AUDIT_READ 37
#mark: CAP_LAST_CAP CAP_AUDIT_READ
- 粒度小
- cap_permitted 表示进程能够使用的权限。但是真正起作用的是 cap_effective。cap_permitted 中可以包含 cap_effective 中没有的权限。一个进程可以在必要的时候,放弃自己的某些权限,这样更加安全。假设自己因为代码漏洞被攻破了,但是如果啥也干不了,就没办法进一步突破。
- cap_inheritable 表示当可执行文件的扩展属性设置了 inheritable 位时,调用 exec 执行该程序会继承调用者的 inheritable 集合,并将其加入到 permitted 集合。但在非 root 用户下执行 exec 时,通常不会保留 inheritable 集合,但是往往又是非 root 用户,才想保留权限,所以非常鸡肋。
- cap_bset,也就是 capability bounding set,是系统中所有进程允许保留的权限。如果这个集合中不存在某个权限,那么系统中的所有进程都没有这个权限。即使以超级用户权限执行的进程,也是一样的。
- 这样有很多好处。例如,系统启动以后,将加载内核模块的权限去掉,那所有进程都不能加载内核模块。这样,即便这台机器被攻破,也做不了太多有害的事情。
- cap_ambient 是比较新加入内核的,就是为了解决 cap_inheritable 鸡肋的状况,也就是,非 root 用户进程使用 exec 执行一个程序的时候,如何保留权限的问题。当执行 exec 的时候,cap_ambient 会被添加到 cap_permitted 中,同时设置到 cap_effective 中。
内存管理
- mm_struct
struct mm_struct * mm;
struct mm_struct * active_mm;
文件与文件系统
/* Filesystem information: */
struct fs_struct * fs;
/* Open file information: */
struct files_struct * files;
用户态的执行和内核态的执行串起来
struct thread_info thread_info;
void * stack;
- 用户态函数栈
- x86
- 
- ESP (Extended Stack Pointer): 栈顶指针寄存器
- 入栈操作 Push 和出栈操作 Pop 指令,会自动调整 ESP 的值
- EBP (Extended Base Pointer): 栈基地址指针寄存器
- 指向当前栈帧的最底部
- A 调用 B,A 的栈里面包含 A 函数的局部变量,然后是调用 B 的时候要传给它的参数,然后返回 A 的地址,这个地址也应该入栈,这就形成了 A 的栈帧。接下来就是 B 的栈帧部分了,先保存的是 A 栈帧的栈底位置,也就是 EBP。因为在 B 函数里面获取 A 传进来的参数,就是通过这个指针获取的,接下来保存的是 B 的局部变量等等。当 B 返回的时候,返回值会保存在 EAX 寄存器中,从栈中弹出返回地址,将指令跳转回去,参数也从栈中弹出,然后继续执行 A。
- x64
- 
- `ax` 保存函数调用的返回结果
- `rsp` 栈顶指针寄存器
- 指向栈顶位置
- 堆栈的 Pop 和 Push 操作会自动调整 rsp
- `rbp` 栈基指针寄存器
- 指向当前栈帧的起始位置
- `rdi`、`rsi`、`rdx`、`rcx`、`r8`、`r9`: 参数传递
- 内核态函数栈
- 
- x86 vs x64
- 
ch04 调度
- 
- 
ch05 进程创建
- 
ch06 线程创建
- 
网络系统
网络分层
两种网络协议模型
应用层 ← 通信 (Socket 系统调用 ) → 内核
Socket 属于操作系统的概念,而非网络协议分层的概念
操作系统选择对网络协议的实现模式
二到四层的处理代码在内核里面
用层例如浏览器、Nginx、Tomcat 都是用户态的
内核里面对于网络包的处理是不区分应用的
四层再往上,就需要区分网络包发给哪个应用
在传输层的 TCP 和 UDP 协议里面,都有端口的概念,不同的应用监听不同的端口
服务端 Nginx 监听 80
Tomcat 监听 8080
客户端浏览器监听一个随机端口
FTP 客户端监听另外一个随机端口
七层的处理代码让应用自己去做
两者需要跨内核态和用户态通信 (Socket)
发送数据包
在客户端浏览器,我们将请求封装为 HTTP 协议,通过 Socket 发送到内核。内核的网络协议栈里面,在 TCP 层创建用于维护连接、序列号、重传、拥塞控制的数据结构,将 HTTP 包加上 TCP 头,发送给 IP 层,IP 层加上 IP 头,发送给 MAC 层,MAC 层加上 MAC 头,从硬件网卡发出去。
网络包会先到达网络 1 的交换机。我们常称交换机为二层设备,这是因为,交换机只会处理到第二层,然后它会将网络包的 MAC 头拿下来,发现目标 MAC 是在自己右面的网口,于是就从这个网口发出去。
网络包会到达中间的 Linux 路由器,它左面的网卡会收到网络包,发现 MAC 地址匹配,就交给 IP 层,在 IP 层根据 IP 头中的信息,在路由表中查找。下一跳在哪里,应该从哪个网口发出去?在这个例子中,最终会从右面的网口发出去。我们常把路由器称为三层设备,因为它只会处理到第三层。
从路由器右面的网口发出去的包会到网络 2 的交换机,还是会经历一次二层的处理,转发到交换机右面的网口。
最终网络包会被转发到 Linux 服务器 B,它发现 MAC 地址匹配,就将 MAC 头取下来,交给上一层。IP 层发现 IP 地址匹配,将 IP 头取下来,交给上一层。TCP 层会根据 TCP 头中的序列号等信息,发现它是一个正确的网络包,就会将网络包缓存起来,等待应用层的读取。
应用层通过 Socket 监听某个端口,因而读取的时候,内核会根据 TCP 头中的端口号,将网络包发给相应的应用。
HTTP 层的头和正文,是应用层来解析的。通过解析,应用层知道了客户端的请求,例如购买一个商品,还是请求一个网页。当应用层处理完 HTTP 的请求,会将结果仍然封装为 HTTP 的网络包,通过 Socket 接口,发送给内核。
内核会经过层层封装,从物理网口发送出去,经过网络 2 的交换机,Linux 路由器到达网络 1,经过网络 1 的交换机,到达 Linux 服务器 A。在 Linux 服务器 A 上,经过层层解封装,通过 socket 接口,根据客户端的随机端口号,发送给客户端的应用程序,浏览器。于是浏览器就能够显示出一个绚丽多彩的页面了。
Function in system-c / cpp
int socket ( int domain , int type , int protocol );
- domain: 使用 IP 层协议
- `AF_INET` 表示 IPv4
- `AF_INET6` 表示 IPv6。
- type: 类型
- `SOCK_STREAM`
- TCP 面向流的
- `SOCK_DGRAM`
- UDP 面向数据报
- `SOCK_RAW`
- 可以直接操作 IP 层,或者非 TCP 和 UDP 的协议
- ICMP
- protocol: 协议
- IPPROTO_TCP
- IPPTOTO_UDP
TCP
bind
IP 地址和端口号
三次握手
客户端和服务端的状态通过三次网络交互,达到初始状态是协同的状态
内核完成
int bind ( int sockfd , const struct sockaddr * addr , socklen_t addrlen );
/* sockfd: socket 文件描述符
*/
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family (IPv4, AF_INET) */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address (IP Address) */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad [ __SOCK_SIZE__ - sizeof ( short int ) -
sizeof ( unsigned short int ) - sizeof ( struct in_addr)];
};
struct in_addr {
__be32 s_addr;
};
- `be`: “big-endian” via [[encoding/character/endian]]
- 如果在网络上传输超过 1 Byte 的类型, 就要区分大端 (Big Endian) & 小端 (Little Endian)
- 客户端不需要 bind?
- 不关心客户端 (不会被访问) 监听到了哪里, 浏览器 随机分配一个端口就可以
- `TCP/IP` 栈: 大端
- `x86`: 小端
- 发出转换
- listen
int listen ( int sockfd , int backlog );
- 服务端只需要调用 accept,等待内核完成了至少一个连接的建立,才返回
int accept ( int sockfd , struct sockaddr * addr , socklen_t * addrlen );
- 如果没有一个连接完成了三次握手,accept 就一直等待
- 如果有多个客户端发起连接,并且在内核里面完成了多个三次握手,建立了多个连接,这些连接会被放在一个队列里面
int connect ( int sockfd , const struct sockaddr * addr , socklen_t addrlen );
- 用到的 socket
- 监听 socket
- 已连接 socket
UDP
对于 UDP 来讲, 甚至都不存在客户端和服务端的概念
只要有一个 socket,多台机器就可以任意通信
每次通信时,调用 sendto 和 recvfrom,都要传入 IP 地址和端口
ssize_t sendto ( int sockfd , const void * buf , size_t len , int flags , const struct sockaddr * dest_addr , socklen_t addrlen );
ssize_t recvfrom ( int sockfd , void * buf , size_t len , int flags , struct sockaddr * src_addr , socklen_t * addrlen );
References