目录
[2.4 进程管理相关的系统调用](#2.4 进程管理相关的系统调用)
[2.4.1 进程复制](#2.4.1 进程复制)
[2.4.2 内核线程](#2.4.2 内核线程)
[2.4.3 启动新程序](#2.4.3 启动新程序)
[2.4.4 退出进程](#2.4.4 退出进程)
本专栏文章将有70篇左右,欢迎+关注,订阅后续文章。
2.4 进程管理相关的系统调用
2.4.1 进程复制
- _do_fork函数
fork vfork clone都最终调用_do_fork
clone:通过CLONE_XX标志精确控制父子进程共享哪些资源。
vfork:由于fork使用了COW技术,vfork优势不再,使用少。
COW:copy-on-write,写时复制。
fork子进程时,使用COW机制,原理:
-
不复制父进程的地址空间。而是将父进程的地址空间标记为只读,并与子进程共享相同的物理内存页。
-
当父进程或子进程有写内存时,发生缺页异常。
-
缺页异常处理中检查该页是否可以写。
若可以,写数据到内存页,再修改子进程页表项。
若不可以,段错误。
COW页:减少不必要的拷贝,提高性能。
- 执行系统调用
cpp
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
stack_start:用户栈
parent_tidptr,child_tidptr:
用于返回线程ID给用户空间,因为pthread_create函数需要tid值
系统调用在用户空间和内核空间传递参数的方法因体系结构而异。
方法有:
寄存器传递:速度快,但寄存器数量有限。
栈传递:可传递内容多。
- do_fork的实现
copy_process:见下节
wake_up_new_task:将该新进程加入调度器队列。
- copy_process 复制进程
dup_task_struct函数:
复制父进程的task_struct和thread_info结构体。
task_struct:存储体系架构无关的通用信息。
thread_info:存储线程的重要信息,不同体系架构定义不一样。从task_struct中独立出来。
通常包含:内核栈栈顶,指向当前线程的task_struct等。
task_struct:存储体系架构无关的通用信息。
创建新进程时分配了新的内核栈,即task_struct->stack
复制后,父子进程两个的task_struct结构体只有一个成员不同:
新进程分配了一个自己的内核栈,即task_struct->stack
cpp
union thread_union {
struct thread_info thread_info; 定义在不同体系中
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
每个进程有一个内核栈,大小为8K。如下:
THREAD_SIZE=8K,即上图内核栈最大为8K,恶意操作内核栈可能覆盖thread_info
cpp
struct thread_info { //以arch/arm为例
unsigned long flags;
int preempt_count; 抢占计数,表示当前线程是否可被抢占。
struct task_struct *task; 代表当前线程
__u32 cpu; 当前线程所在CPU
struct cpu_context_save cpu_context; 保存着CPU寄存器(如PC,SP等)
};
其中thread_info中flag有:
TIF_SIGPENDING 当前进程是否有待决信号
TIF_NEED_RESCHED 当前进程想让出CPU,调度器选择其他进程执行。
TIF = Thread Info Flag
如何访问指定线程的thread_info?
(struct thread_info *) (task)->stack
如何根据当前线程thread_info找到当前线程的task_struct?
task_struct *current = current_thread_info()->task
如何访问当前线程的thread_info?
cpp
struct thread_info *current_thread_info(void) ARM为例
{
register unsigned long sp asm ("sp"); //sp寄存器:保存了当前线程的内核栈顶部
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
如何根据thread_info找到对应task_struct?
task_struct *current = current_thread_info()->task
task_struct->stack和CPU sp寄存器,如上图,两者不指向同一地址:
task_struct->stack:
指向创建该线程时分配8K内核栈的起始地址。也就是thread_info处
CPU sp寄存器:
当前CPU运行线程的内核栈栈顶。
当前进程正在运行时:
通过ARM sp寄存器值,得到当前线程的thread_info,再得到current的task_struct。
进程切换到一个新进程时:
通过task_strcut -> stack,得到该线程的thread_info,再通过thread_info得到cpu_context,即可得到该进程上次执行时的寄存器信息,如pc,sp,r0-r12等。
进程切换时,关于进程的task_struct的stack成员,sp寄存器,变化过程?
- 保存当前进程的上下文:
保存当前进程上下文到内核栈中:包括CPU的通用寄存器、程序计数器PC、栈指SP等。
- 切换新进程的:
切换到新进程的task_struct结构体,再通过task_struct->stack得到thread_info。
- 恢复新进程上下文
从thread_info中cpu_context得到该进程上次执行时的上下文信息。如pc,sp,r0-r12等。从而恢复新进程上下文值。此时可正确得到新进程的内核栈栈顶sp。
struct pt_regs 和 thread_info中struct cpu_context_save 是用于保存 CPU 寄存器状态
区别:
struct pt_regs:用于处理异常或系统调用返回时将其恢复到原始状态,还可传参。
struct cpu_context_save:用于进程切换时主动保存CPU上下文。
kstack_end(void *addr)函数:
返回当前线程的内核栈的结束地址。
这样就可判断某个地址是否在内核栈区间。
继续回到copy_process
sched_fork函数:
-
初始化子进程调度参数:优先级和调度策略等。
-
复制父进程的调度器相关数据(调度器类别,时间片)。
-
将子进程加入调度队列。
copy_process会检测如下标志:
CLONE_FS 共享父进程的文件系统
CLONE_NEWXX 不共享的资源
CLONE_FILES 共享父进程的文件描述符
CLONE_SIGHAND 共享父进程的信号处理函数
CLONE_MM COW,只复制页表
struct pt_regs { 如上图,存储在当前线程的内核栈最底部中。
long uregs[18];
};
struct pt_regs作用:
从用户态陷入内核态时候,用户态的上下文信息保存在pt_regs数据结构中。还可传递系统调用参数和返回值。
存储的寄存器信息有:
#define ARM_cpsr uregs[16] 程序状态寄存器
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13] 当前线程内核栈的栈顶
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10] //通用寄存器 r0-r10
struct pt_regs这18个寄存器,保存在当前线程的内核栈的底部,如上图。
即 :struct pt_regs *regs = task_struct->stack + THREAD_START_SP - 1
copy_process还调用copy_thread。
copy_thread重要内容:
填充thread_info和pt_regs。
父子进程可共享信号处理函数,但不共享挂起待处理信号。
unsigned long put_user(void __user *dst, const void *src, unsigned long size);
向用户空间传递单个数据。如char,short,int大小的数据,比copy_to_user快。
copy_to_user优点:可复制任意类型和长度数据。
每个体系的虚拟地址0到4KB的区域,没有任何意义。可重用该地址范围来编码错误码。
如果返回值指向0-4KB地址范围内部,表示该调用失败,其原因由指针值判断。
宏ERR_PTR:将数值常数编码为指针。
使用方法:return ERR_PTR(-EINVAL);
2.4.2 内核线程
内核线程父进程是:init进程
内核线程的任务通常是周期任务,如:
pdflush:刷新脏页到磁盘。
kswapd:回写内存页到交换区。
ksoftirqd:处理软中断。
创建内核线程:
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
最终也调用_do_fork(CLONE_VM)
创建的内核线程在指定CPU上运行:
kthread_create_on_cpu()
-> p->sched_class->set_cpus_allowed(p, new_mask);
kthread_run() = kthread_create() + wake_up_process()
内核线程不需要用户空间,所以内核线程task_struct的mm_struct=NULL。
当内核线程运行,可不置换掉之前进程的用户空间地址,因为内核线程不使用用户空间。所以用active_mm保存用户空间mm_struct,因为内核线程运行后调度的进程通常还是之前那个用户进程,通过active_mm直接恢复,不用修改映射表,TLB中缓存的映射表仍然有效。这叫惰性TLB。
惰性TLB:一种优化策略,延迟或避免不必要TLB的更新,提高性能。
TASK_SIZE:即用户态虚拟地址大小(32位,0-3G)。
内核线程地址空间大于TASK_SIZE。
2.4.3 启动新程序
execve系统调用
int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp)
会__user定义的指针进行参数检查。
linux_binfmt存储了所有注册的可执行程序的加载函数和执行函数。
struct linux_binprm:保存可执行文件的信息,包括可执行程序的路径,参数和环境变量的信息,vma
cpp
struct linux_binfmt {
struct list_head lh; 连接所有二进制的执行函数
int (*load_binary)(struct linux_binprm *); 加载二进制文件
int (*load_shlib)(struct file *); 加载动态库
int (*core_dump)(struct coredump_params *cprm); 用于crash时核心转储文件
}
Linux文件特殊权限SUID、SGID、Sticky总结:
SUID文件所属主:Set User ID
当一个可执行文件具有SUID权限时,它执行时临时具有文件所有者的权限,而不是执行者的权限。
作用:暂时提升用户权限。允许普通用户执行root用户的程序。
缺点:潜在安全性威胁。谨慎使用。
使用举例:
/usr/bin/passwd:允许用户更改自己的密码而无需root权限。
设置方法:
增加suid权限:chmod u+s ,或chmod 4755
移除suid权限:chmod u-s ,或chmod 0755。
SGID文件属组: Set Group ID
当一个文件或目录设置SGID权限后,任何用户执行该文件或访问该目录时,都以该文件或目录所属的组身份执行,而不是该用户的组权限。
使用场景:当不同组的用户在一个共享目录下创建新文件,新文件是该目录所属组的权限,而不是创建文件的用户的组权限。可确保所有用户以相同的组权限执行该目录下新文件。
设置方法:
增加suid权限:chmod g+s ,或chmod 2755。
移除sgid权限:chmod g-s ,或chmod 0755。
Sticky权限:
作用:一般用于目录,只允该目录下的文件的创建者删除自己的创建的文件,不允许其他人删除文件。
二进制文件起始处的magic值可标识该文件类型。
如:ELF可执行文件:Magic number: 0x7F ELF
JPEG图像文件:Magic number:0xFFD8FF
search_binary_hander:
根据文件起始处的magic值来查找对应二进制文件的加载,执行函数。
二进制加载函数: 将文件段映射到虚拟地址空间。
最终给变量start_code,end_code,start_data,end_data,start_brk brk,start_stack,arg_start,arg_end赋值。
每种二进制格式通过register_binfmt注册:
如script_format,elf_format,aout_format等
2.4.4 退出进程
exit
各种引用计数减1。减1后若等于0,释放资源。