趣谈Linux4
进程
写代码:用系统调用创建进程
process.c函数
1 |
|
createprocess.c函数
1 | // createprocess.c |
进行编译:程序的二进制格式
- Linux下面,二进制的程序有严格的格式,即为ELF(Executeable and Linkable Format,可执行与可链接格式)
- 根据编译结果的不同,分为不同的格式
| ELF格式 | 文件后缀 | |
|---|---|---|
| 可重定位文件 | Relocatable File | .o |
| 可执行文件 | Executeable File | 无后缀 |
| 共享对象文件 | Share Object | .so |

使用gcc进行编译
1 | gcc -c -fPIC process.c |
- 在编译的时候,会先做预处理工作
- 如将头文件嵌入到正文中,将定义的宏展开
- 然后是真正的编译过程
- 最后编译成为.o文件,这就是ELF的第一种类型,可重定位文件(Relocatable
File)
- 这里会生成process.o和createprocess.o两个文件
可重定位文件
文件格式如下:

ELF文件的头是用于描述整个文件的,文件格式在内核中有定义,分别为strcut elf32_hdr和strcut elf64_hdr,ELF文件一个一个的section,叫做节
- .text:放编译好的二进制可执行文件
- .data:已经初始化好的全局变量
- .rodata:只读数据,例如字符串常量、const的变量
- .bss:未初始化全局变量,运行时会置0
- .symtab:符号表,记录的则是函数和变量
- .strtab:字符串表、字符串常量和变量名
注意点:为什么这里只有全局变量?
局部变量是放在栈里面的,是程序运行过程中随时分配空间,随时释放的,现在讨论的是二进制文件,还没有启动,只需讨论在哪里保存全局变量
- 这些节的元数据信息需要有一个地方保存,就是最后的节头部表(Section Header Table)
- 在表里,每一个section都有一项,在代码里面也有定义的struct elf32_shdr和struct elf64_shdr
- 在ELF的头里面,有描述这个文件的节头部表的位置,有多少个表项等信息
可重定位的意义
编译好的代码和变量,将来加载到内存里面的时候,都是要加载到一定位置的,但是现在还是一个.o文件,不是一个可直接运行的程序,只是部分代码片段,将来被谁调用,在哪里调用都不清楚,所以.o里面的位置是不确定的,但是必须是可重新定位的,因为它将来是要做函数库的
.rel.text和.rel.data与重定位有关
要想让create_process函数作为库文件被重用,不能以.o的形式存在,而是要形成库文件,最简单的类型是静态链接库.a文件(Archives),仅仅将一系列对象文件(.o)归档为一个文件,使用命令ar创建
1 | ar cr libstaticprocess.a process.o |
这里可以有多个.o,当有程序要使用这个静态链接库的时候,会将.o文件提取出来,链接到程序中
1 | gcc -o staticcreateprocess createprocess.o -L. -lstaticprocess |
在这个命令中,-L表示在当前目录下找.a文件,-lstaticprocess会自动补全文件名,比如加前缀lib,后缀.a,变成libstaticprocess.a
找到这个.a文件后,将里面的process.o取出来,和createprocess.o做一个链接,形成二进制执行文件staticcreateprocess
这个链接的过程,重定位就起作用了,createprocess.o将process.o合并进来,就知道create_process函数的位置了
可执行文件
形成的二进制文件叫可执行文件,是ELF的第二种格式
文件格式如下:

与.o文件大致相似,分成一个个的section,并且被节头表描述
这些section是多个.o文件合并过的
这个时候,这个文件已经是马上就可以加载到内存里面执行的文件了,因此这些section被分成了几个部分
- 需要加载到内存里面的代码段、数据段
- 不需要加载到内存里面的部分
将小的section合成了大的段segment,并且在最前面加一个段表头(Segment Header Table)
在代码里面的定义为struct elf32_phdr和struct elf64_phdr,这里面除了有对于段的描述之外,最重要的是p_vaddr,这个是这段加载到内存的虚拟地址
在ELF头里面,有一项e_entry,也是个虚拟地址,是这个程序运行的入口
运行该程序后,会执行ls命令
1 | [root@localhost ~]# ./staticcreateprocess |
共享对象文件
静态链接库一旦链接进去,代码和变量的section都合并了,因而程序运行的时候,就不依赖这个库是否存在
- 但有一个缺点,就是相同代码段,被多个程序使用,在内存里面就有多份
- 而且一旦静态链接库更新,如二进制执行文件不重新编译,也不会随着更新
因此,出现了动态链接库(Shared Lirary)
- 不仅仅是一组对象文件的简单归档,而是多个对象文件的重新组合,可被多个程序共享
1
gcc -shared -fPIC -o libdynamicprocesss.so process.o
注意点:
当一个动态链接库被链接到一个程序文件中时,最后的程序文件并不包括动态链接库中的代码,而仅仅包括对动态链接库的引用,并且不保存动态链接库的全路径,仅仅保存链接库的名称
创建可执行文件
1
gcc -o dynamiccreateprocess createprocess.o -L. -ldynamicprocess
当运行这个程序的时候,首先寻找动态链接库,然后加载它
- 默认情况下,系统在/lib和/usr/lib文件夹下寻找动态链接库,找不到就会报错
- 可以设定LD_LIBRARY_PATH环境变量,程序运行时会在此环境变量指定的文件夹下寻找动态链接库
1 | [root@localhost ~]# export LD_LIBRARY_PATH=. |
- 基于动态链接库创建出来的二进制文件格式还是ELF,但是稍有不同
- 首先,多了一个.interp的Segment,这里面是Id-linux.so,这是动态链接器,运行时的链接动作都是它做的
- 另外,ELF文件中还多了两个section
- 一个是.plt,过程链接表(Proced Linkage Table,PLT)
- 一个是.got.plt,全局偏移量表(Global Offset Table,GOT)
- 程序运行的时候,它们是如何将so文件动态链接到进程空间的?
- dynamiccreateprocess这个程序要调用libdynamicprocess.so里的create_process函数
- 由于是运行时才去找,编译的时候不知道函数在哪里,所以就在PLT里面建立一项PLT[x]
- 这一项是一些代码,有点像一个本地的代理,在二进制程序里面,不直接调用create_process函数,而是调用PTL[x]里面的代理代码,这个代理代码会在运行的时候找真正的create_process函数
- 会使用GOT来找代理代码,这里面也会为create_process函数创建一项GOT[y]
- 这一项是运行时create_process函数在内存中真正的地址
- 如果这个地址在,dynamiccreateprocess调用PLT[x]里面的代理代码,代理代码调用GOT表中对应项GOT[y],调用的就是加载到内存中的libdynamicprocess.so里面的create_process函数了
- 对于create_process函数,GOT一开始就会创建一项GOT[y],但是这里面没有真正的地址,因为它也不知道,但它又回调PLT,告诉它,你里面的代码代理来找我要create_process函数的真实地址,但我不知道。
- PLT这个时候会转而调用PLT[0],即第一项,PLT[0]转而调用GOT[2],这里面是ld-linux.so的入口函数
- 这个函数会找到加载到内存中的libdynamicprocess.so里面的create_process函数的地址,然后把这个地址放在GOT[y]里面
- 下次,PLT[x]的代理函数就能够直接调用了
运行程序为进程
我们现在知道了ELF这个格式,但这个时候它还是个程序,如何将这个文件加载到内存里面
- 在内核中,有一个数据结构被用来定义加载二进制文件的方法
1 | struct linux_binfmt { |
- 对于ELF文件格式,有对应的实现
1 | static struct linux_binfmt elf_format = { |
其中,load_elf_binary我们在加载内核镜像的时候,用的也是这种格式。
当前的具体调用过程为:do_execve -> do_execveat_common -> exec_binprm -> search_binary_hander.
调用do_execve的是SYSCALL_DEFINE3函数,原理是exec这个系统调用最终调用的load_elf_binary
- exec比较特殊,它是一组函数:
- 包含p的函数(execvp,execlp)会在PATH路径下面寻找程序
- 不包含p的函数需要输入程序的全路径
- 包含v的函数(execv,execvp,execve)以数组的形式接受参数
- 包含l的函数(execl, execlp, execle)以列表的形式接受参数
- 包含e的函数(execve, execle)以数组的形式接受环境变量
进程树
所有的进程都是从父进程fork过来的,总有一个祖宗进程,就是系统启动的init进程
解析Linux的启动过程中,1号进程是/sbin/init
- 如果在CentOS 7里面,我们ls查看,可以看到,这个进程是被软链接到systemd的
1
2[root@localhost ~]# ls -la /sbin/init
lrwxrwxrwx. 1 root root 22 Apr 10 03:53 /sbin/init -> ../lib/systemd/systemd

- 系统启动之后,init进程会启动很多的daemon进程,为系统运行提供服务,然后启动getty,让用户登录,登录后运行shell,用户启动的进程都是通过shell运行的,从而形成了一棵进程树
- 通过ps -ef命令查看当前系统启动的进程
- PID 1的进程是init进程systemd
- PID 2的进程是内核线程kthreadd
- 用户态的不带中括号,内核态的带中括号
- 进程号依次增大,但是会看到所有带中括号的内核态的进程,祖先都是2号进程,用户态的进程的祖先都是1号进程
- TTY一列,是问号的,说明不是前台启动的,一般都是后台的服务
- pts的父进程是sshd,bash的父进程是pts,ps -ef的父进程是bash
1 | [root@localhost ~]# ps -ef |
小结

- 首先通过图右边的文件编译过程,生成so文件和可执行文件,放在硬盘上
- 左边的用户态的进程A执行fork,创建进程B
- 在进程B的处理逻辑中,执行exec系列的系统调用
- 这个系统调用会通过load_elf_binary方法,将刚才生成的可执行文件,加载到进程B的内存中执行
线程
为什么需要线程
- 对于任何一个进程,即使我们没有主动去创建线程,进程也是默认有一个主线程的
- 线程是负责执行二进制指令的
- 进程要比线程管的多,除了执行指令之外,内存、文件系统等都要它来管
进程相当于一个项目,线程就是为了完成需求,建立的一个个开发任务
- 使用进程实现并发执行有两个主要问题
- 第一,创建进程占用资源太多
- 第二,进程之间的通信需要数据在不同的内存空间传来传去,无法共享
创建进程
1 |
|
一个运行中的线程可以调用pthread_exit退出线程
- 该函数可以传入一个参数转换为(void *)类型,是线程退出的返回值
主线程里
列了5个文件名,声明了一个数组,里面有5个pthread_t类型的线程对象
声明了一个线程属性pthread_attr_t,通过pthread_attr_init初始化这个属性,并且设置属性PTHTREAD_CREATE_JOINABLE,表示将来主线程等待这个线程的结束,并获取退出时的状态
接下来是一个循环,对于每一个文件和每一个线程,可以调用pthread_create创建线程。
- 一共有4个参数,第一个参数是线程对象,第二个参数是线程的属性,第三个参数是线程运行函数,第四个参数是线程运行函数的参数
- 主线程就是通过第4个参数,将自己的任务派给子线程
任务分配完成后,每个线程下载一个文件,主线程需要做的事情就是等待这些子任务完成
当一个线程退出的时候,就会发送信号给其他所有同进程的线程
使用pthread_join获取这个线程退出的返回值,线程的返回值通过pthread_join传给主线程,这样子线程就将自己下载文件所消耗的时间,告诉给主线程
多线程程序要依赖于libpthread.so
1 | gcc download.c -lpthread |
编译后执行
1 | [root@localhost thread]# ./a.out |
小结

线程的数据
线程访问的数据细分为3类
线程栈上的本地数据,如函数执行过程中的局部变量
- 栈的大小可以通过命令ulimit -a查看,默认情况下线程栈大小为8192(8MB)
- 可通过命令ulimit -s修改
- 对于线程栈,可通过函数pthread_attr_setstacksize,来修改线程栈的大小
1
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
- 主线程在内存中有一个栈空间,其他线程栈也拥有独立的栈空间
- 为避免线程之间的栈空间踩踏,线程栈之间还会有小块区域用来隔离保护各自的栈空间,一旦另一个线程踏入隔离区,会引发段错误
- 栈的大小可以通过命令ulimit -a查看,默认情况下线程栈大小为8192(8MB)
在整个进程里共享的全局数据,如全局变量
- 如果同一个全局变量,两个线程一起修改,肯定会有问题,需要有一种机制保护他们
线程私有数据(Thread Specific Data)
- 可通过以下函数创建
1
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*))
创建一个key,伴随着一个析构函数
一旦key被创建,所有线程都可访问它,但各线程可根据自身需要往key中填入不同的值,相当于提供了一个同名而不同值的全局变量
可通过下面的函数设置key对应的value
1
int pthread_setspecific(pthread_key_t key, const void *value)
- 获取key对应的value
1
void *pthread_getspecific(pthread_key_t key)
数据保护
互斥(Mutex,Mutual Exclusion)
- 在共享数据访问的时候,申请加把锁,谁拿到锁,谁就有访问权限,遵循谁先拿到谁访问
1 |
|
如果没有加上mutex,那么中间的状态会很不正确,会出现超过200的情况
加上mutex后,可以保证两者之和永远都是200
使用Mutex,首先要使用pthread_mutex_init函数初始化这个mutex,初始化后,用它保护共享变量
pthread_mutex_lock()是抢那个锁的函数,抢到了,可以执行下一行程序,对共享变量进行访问;没抢到,被阻塞在那里瞪大
如不想被阻塞,可以使用pthread_mutex_trylock去抢锁
共享数据访问结束后,使用pthread_mutex_unlock释放锁,让给其他人使用
最后调用pthread_mutex_destroy销毁锁
小结

- 如果使用pthread_mutex_lock(),需要一直在那里等待
- 如果是pthread_mutex_trylock(),就不用等待,可以去干点别的
- 需要条件变量来进行通知,条件变量和互斥锁是配合使用的
条件变量
1 |
|
- 有10个任务,每个任务一个字符
- 有两个变量head和tail,表示当前分配的工作从哪里开始,到哪里结束
- 如果head等于tail,则当前的工作分配完毕
- 如果tail加N,就是新分配了N个工作

总结

进程数据结构
有的线程只有一个线程,有的进程有多个线程,它们都需要由内核分配CPU来干活。但是CPU总共就那么几个,应该怎么管理,怎么进行调度呢
- 在Linux里面,无论是进程,还是线程,到了内核里面,我们统一都叫任务(Task),由一个统一的结构task_struct进行管理

- 首先,Linux内核应该先弄一个链表,将所有的task_struct串起来
1 | struct list_head tasks |
任务
任务ID
每一个任务都应该有一个ID,作为任务的唯一标识
task_struct里面涉及任务ID的,有以下几个
1 | pid_t pid; |
为什么会有多个标识呢?是因为上面的进程和线程到了内核这里,统一变成了任务,带来了两个问题
- 任务展示
- 如ps命令可以展出所有的进程,到了内核,按照任务列表展出的话,所有的线程也会平摊开来
- 给任务下发指令
- 如kill命令可以给进程发信号,通知进程退出。如果只发给了其中一个线程,我们就不能只退出这个线程,而是应该退出整个进程,当然有时候,希望只给某个线程发信号
所以,在内核中,进程和线程虽然都是任务,但是应加以区分。
其中,pid是process id,tgid是thread group ID
- 任何一个进程,如果只有主线程,那pid是自己,tgid是自己,group_leader指向的还是自己
- 如果一个进程创建了其他线程,那么,线程有自己的pid,tgid就是进程的主线程的pid,group_leader指向的就是进程的主线程
信号处理
task_struct里面关于信号处理的字段
1 | struct signal_struct *signal; |
- 定义了哪些信号被阻塞暂不处理(blocked),哪些信号尚等待处理(pending),哪些信号正在通过信号处理函数进行处理(sighand)
- 处理结果可以是忽略,可以使结束进程等等
- 信号处理函数默认使用用户态的函数栈,也可以开辟新的栈专门用于信号处理,这就是sas_ss_xxx这三个变量的作用
- 我们有一个struct sigpending pending,进入struct signal_struct *signal里面看的话,还有一个struct sigpending shared _pending,它们一个是本任务的,一个是线程组共享的
任务状态
在task_struct里面,涉及任务状态的是下面几个变量
1 | volatile long state; |
state(状态)可以取的值定义在include/linux/sched.h头文件中
1 | /* Used in tsk->state: */ |
从定义的数值很容易看出,flags是通过bitset的方式设置的,也就是说,当前时什么状态,哪一位就至1(二进制)

TASK_RUNNING并不是说进程正在运行,而是表示进程在时刻准备运行的状态
- 当处于这个状态的进程获得时间片的时候,就是在运行中
- 如果没有获得时间片,就说明它被其他进程抢占了,在等待再次分配时间片
在运行的过程,一旦要进行一些I/O操作,需要等待I/O完毕,这个时候会释放CPU,进入睡眠状态
Linux中有两种睡眠状态
- 一种是
TASK_INTERRUPTIBLE,可中断的睡眠状态。是一种浅睡眠的状态,也就是说,虽然在睡眠,等待I/O完成,但是这个时候一个信号来的时候,进程还是要被唤醒
- 但是唤醒后,不是继续刚才的操作,而是进行信号处理
- 当然,程序员可以根据自己的意愿,来写信号处理函数,例如收到某些信号,就放弃等待这个I/O操作完成,直接退出;也可收到某些信息,继续等待
- 另一种是
TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。是一种深度睡眠状态,不可被信号唤醒,只能死等I/O操作完成。一旦I/O操作因为特殊原因不能完成,谁也叫不醒这个进程了。
- kill本身也是一个信号,kill信号也会被忽略,除非重启电脑
- 一种是
TASK_INTERRUPTIBLE,可中断的睡眠状态。是一种浅睡眠的状态,也就是说,虽然在睡眠,等待I/O完成,但是这个时候一个信号来的时候,进程还是要被唤醒
有了一种新的进程睡眠状态,TASK_KILLABLE,可以终止的新睡眠状态,进程处于这种状态中,运行原理类型与不可中断的睡眠状态,只不过可以响应致命信号
- TASK_WAKEKILL用于在接受到致命信号时唤醒进程,而TASK_KILLABLE相当于这两位都设置了
1
TASK_STOPPED是在进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后进入该状态
TASK_TRACED表示进程被debugger等进程监视,进程执行被调试程序所停止
- 当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态
一个进程结束的时候,先进入的是EXIT_ZOMBIE状态,但是此时它的父进程还没有使用wait()等系统调用来获知它的终止信息,此时进程就成了僵尸进程
- EXIT_DEAD是进程的最终状态
- EXIT_ZOMBIE和EXIT_DEAD也可以用于exit_state
上面的进程状态和进程的运行、调度有关系,还有其他的一些状态,称为标志,放在flags字段中,这些字段被定义称为宏,以PF开头,下面是几个例子
1 |
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 去掉。
进程调度
进程的状态切换往往涉及调度。
下面是一些关于调度的字段
1 | // 是否在运行队列上 |
小结

运行统计信息
在进程的运行过程中,会有一些统计量,具体可看下面列表,有进程在用户态和内核态消耗的时间、上下文切换的次数等等
1 | u64 utime; // 用户态消耗的CPU时间 |
进程亲缘关系
任何一个进程都有父进程,所以,整个进程其实就是一棵进程树,而拥有同一父进程的所有进程都具有兄弟关系
1 | struct task_struct __rcu *real_parent; /* real parent process */ |
- parent 指向其父进程。当它终止时,必须向它的父进程发送信号。
- children 表示链表的头部。链表中的所有元素都是它的子进程。
- sibling 用于把当前进程插入到兄弟链表中。

- 通常情况下,real_parent和parent是一样的,但也有另外的情况存在
- 例如,bash创建一个进程,那进程的 parent 和 real_parent 就都是 bash
- 如果在 bash 上使用 GDB来 debug 一个进程,这个时候 GDB 是 real_parent,bash 是这个进程的 parent
进程权限
在Linux里面,对于进程权限的定义如下:
1 | /* Objective and real subjective task credentials(COW): */ |
- Objective是被操作的对象,而Subjective是进行操作的对象
- 操作,就是一个对象对另一个对象进行某些动作
- 当动作要实施的时候,就要审核权限,当两边的权限匹配上,才可以实施动作
- 其中,read_cred是说明被操作的进程,cred是进行操作的进程
- cred的定义如下,大部分是用户和用户所属的用户组信息
1 | struct cred { |
- 第一个是uid和gid,注释是read user/group id。
- 一般情况下,谁启动的进程,就是谁的ID
- 但是权限审核的时候,往往不比较这两个,说明不大起作用
- 第二个是euid和egid,注释时effective user/group id
- 是起作用的ID
- 当这个进程要操作消息队列、共享内存、信号量等对象的时候,其实就是在比较这个用户和组是否有权限
- 第三个是fsuid和fsgid,也就是filesystem user/group id
- 这个是对文件操作会审核的权限
一般来说,fsuid、euid和uid是一样的,fsgid、egid和gid也是一样的
因为谁启动的进程,就应该审核启动的用户到底有没有这个权限
特殊情况
以用户和用户组控制权限

- 用户A想运行用户B安装的游戏程序(权限为rwxr--r--),但A是没有权限运行该程序的,因此B必须要给用户A权限才行,设定程序为所有用户都能执行(权限为rwxr-xr-x)
- 用户A便可以运行游戏程序,游戏运行时,游戏进程的uid、euid、fsuid都是用户A
- 但是,用户A想保存通关数据的时候,发现游戏的玩家数据时保存在另一个文件里面的(权限为rw-------),只给用户B开了写入权限,而游戏进程的euid和fsuid都是用户A,写不进去
- 可以使用chmod u+s program,给游戏程序设置set-user-ID的标识位,将游戏的权限变为rwsr-xr-x
- 当用户A再启动游戏时,创建的进程uid是用户A,但是euid和fsuid是用户B,这样可以将游戏数据保存下来
在Linux里面,一个进程可以随时通过setuid设置用户ID,所以游戏程序的用户B的ID还会保存在一个地方,就是suid和sgid,这样就可以方便地使用setuid,通过设置uid或者suid来改变权限
capabilities机制
- 控制进程的权限,要么是高权限的root用户,要么是一般权限的普通用户
- 这时候的问题是,root用户权限太大,普通用户权限太小
- 有时候普通用户向做一点高权限的事情,得给他整个root的权限,实在不安全
- 因此引入新的机制capabilities,用位图来表示权限,可在capability.h找到定义的权限
1 |
|
- 对于普通用户运行的进程,当有这个权限的时候,就能做相应的操作
- cap_permitted:表示进程能够使用的权限,但真正起作用的是cap_effective
- cap_permitted中可以包含cap_effective中没有的权限
- 一个进程可以在必要的时候,放弃自己的某些权限,这样更加安全
- cap_inheritable:表示当可执行文件的扩展属性设置了inferitable位时,调用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。
ps. 后面内存管理具体讲述
1 | struct mm_struct *mm; |
文件与文件系统
每个进程有一个文件系统的数据结构,还有一个打开文件的数据结构
ps. 后面文件系统具体讲述
1 | /* Filesystem information: */ |
小结

在程序执行过程中,一旦调用到系统调用,就需要进入内核继续执行。如何将用户态的执行和内核态的执行串起来呢?
需要两个重要的成员变量
1 | struct thread_info thread_info; |
用户态函数栈
在用户态中,程序的执行往往是一个函数调用另一个函数。
函数调用都是通过栈来进行的。
- 在进程的内存空间里面,栈是一个从高地址到低地址,往下增长的结构
- 也就是说,上面是栈底,下面是栈顶
- 入栈和出栈的操作都是从下面的栈顶开始的

32位操作系统
在CPU里,ESP是栈顶指针寄存器
