目录
前言
2.1 进程优先级
2.2 进程生命周期
2.3 进程表示
2.3.1 进程类型
2.3.2 命名空间
2.3.3 进程ID号
2.3.4 进程关系
2.4 进程管理相关的系统调用
2.4.1 进程复制
2.4.2 内核线程
2.4.3 启动新程序
2.4.4 退出进程
前言
本章内容太多,分为两篇博文。这是第一部分(1)
root用户的UID=0。
两个系统调用:
chroot函数:
含义:更改当前进程的根目录,限制进程只能访问新根目录下文件。
作用:
安全隔离,即使进程被攻击,也无法访问系统的其他部分。
在开发测试中,用来创建一个独立的文件系统环境,而不会影响主系统。
chdir函数:
含义:更改进程当前工作目录,影响了相对路径解析起点,但不限制文件访问范围。
作用:简化路径解析,方便文件操作。
ELF:(Executable and Linking Format)
Linux中可执行文件或共享库的标准格式。
2.1 进程优先级
进程分类:
硬实时进程:
含义:必须在预定时间内完成任务。
使用场景:航空航天、汽车控制、工业自动化等。
Linux不支持,而RTLinux支持。
软实时进程:
尽力在预定时间内完成任务。Linux支持。
普通进程:
大部分进程都是,Linux支持。
实时进程的优先级比普通进程高。
而进程的优先级最终决定了进程运行的时间片比例,和调度先后顺序。
每个进程都有一个内核栈(8K或16K),用于存储该进程在内核态执行的函数参数、局部变量、寄存器等。
不同进程的内核栈不会重叠,但都在地址空间3-4G范围内(32位系统)。
2.2 进程生命周期
5个进程的状态:
运行态(R):
已在CPU的运行队列上,但不一样正在CPU上运行。
可中断休眠(S):
被阻塞,等待到资源就绪后,被唤醒。
不可中断休眠(D):
深度休眠,不被中断。可确保关键操作的完整和可靠,
使用场景:保证重要磁盘IO操作不被中断。
停止态(T):
SIGSTOP信号可停止进程。SIGCONT信号让进程继续运行。
使用场景:gdb断点。
僵尸态:
已终止但未被父进程使用wait() 或 waitpid()处理。
僵尸进程占用了系统的进程表项,耗尽资源。
kernel preemption:内核抢占
2.3 进程表示
内核用struct task_struct结构体表示一个进程。
struct task_struct {
volatile long state; 进程状态:运行态,僵尸态,停止态,可中断休眠,不可中断休眠
void *stack; 指向该进程的内核栈的顶部
int prio,static_prio,normal_prio; 进程优先级,2.5章节详细
unsigned int rt_priority; 实时调度器使用
unsigned int policy; 调度策略:如SCHED_NORMAL/SCHED_RR
struct sched_class *sched_class; 一个调度器的函数指针
struct sched_entity se; 调度信息,嵌入到CFS调度红黑树
cpumask_t cpus_allowed; 允许进程在哪些CPU上运行
struct mm_struct *mm, *active_mm; 该进程的进程地址空间(用户空间)
pid_t pid;
pid_t tgid; 即getpid函数返回值
struct task_struct _rcu *real_parent;
struct task_struct __rcu *parent; 父进程
struct pid_link pids[PIDTYPE_MAX];
struct fs_struct *fs; 文件系统信息
struct files_struct *files; 包含该进程所有文件描述符
struct nsproxy *nsproxy; 命名空间
struct signal_struct *signal; 信号描述信息
struct sighand_struct *sighand; 信号处理函数,线程组间可共享信号处理函数。
}
task_struct成员介绍:
struct task_struct *real_parent;
当ptrace跟踪进程时,该进程的parent指向ptrace进程,所以需用real_parent保存真实父进程。
struct mm_struct *active_mm;
当内核线程抢占用户进程后,内核线程的mm成员为NULL,需要通过该变量可知道抢占了哪个用户进程。
图片总结:
task_struct 中
struct fs_struct *fs;
struct files_struct *files;
进程state为TASK_UNINTERRUPTIBLE的进程不能被信号唤醒,只能内核亲自唤醒。
一个进程打开最大文件数目默认是1024。
查看限制:ulimit -n
统计该进程已经打开文件数目:ls /proc//fd | wc -l
资源限制
struct rlimit{
unsigned long rlim_cur; 软限制。超过软限制但未到硬限制时,会收到警告。
unsigned long rlim_max; 硬限制。强制不能超过硬限制。
}
struct rlimit被谁包含:
struct task_struct 中成员s truct signal_struct *signal;
struct signal_struct中成员struct rlimit rlim[RLIM_NLIMITS];
如:
一个进程能创建的最大文件大小:
current->signal->rlim[RLIMIT_FSIZE].rlim_cur
init进程可拥有的最大子进程数量:
init_task.rlim[RLIMIT_NPROC].rlim_cur = max_threads/2;
全局变量max_threads:把八分之一可用内存用于管理线程信息时,可创建最多线程数目。
相关命令:
#cat /proc/259/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max stack size 8388608 unlimited bytes
Max processes 257417 257417 processes
setrlimit系统调用:
作用:设置该进程的资源限制。如:
最大CPU时间,最大文件长度,数据段最大长度,用户栈最大长度,打开文件最大数目,待决信号最大数目,最大实时优先级,不可换出页最大数目(mlock)
int mlock(const void *addr, size_t len);
将进程指定的内存区域锁定在物理内存中,防止被内核交换出去,可保证该内存区域访问速度更快。
使用场景:高性能的应用程序,比如实时应用程序、高性能数据库等。
SIGQUEUE_MAX:待决信号队列大小。默认32或64,高性能场景值更大,是系统级参数,对所有进程生效。
2.3.1 进程类型
2.3.2 命名空间
命名空间部分稍显复杂。
命名空间提供了一种轻量级虚拟机机制。
把所有全局资源都通过命名空间抽象起来,起到隔离作用。
实现:
struct task_struct {
struct nsproxy *nsproxy; 该进程的命名空间。如上图多个进程可共享同一命名空间。
}
struct nsproxy {
struct uts_namespace *uts_ns; //UTS命名空间包含:内核版本、底层体系等。
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns; 挂载信息
struct pid_namespace *pid_ns; pid
struct net *net_ns;
}
一个全局命名空间:init命令空间
struct nsproxy init_nsproxy;
所有进程在创建时,默认为init_nsproxy命令空间。
UTS命名空间:
即struct uts_namespace。
不需特别处理,是简单信息,没有层次和父子关系。
UTS:UNIX Timesharing System
struct uts_namespace{
struct kref kref; 该结构实例引用计数
struct new_utsname name;
};
struct new_utsname { 就是系统信息
char sysname[65]; //不能修改,都是"Linux"
char release[65];
char version[65];
char machine[65];
};
如何创建新命名空间:
1. fork时根据标志是共享命名空间,还是新建命名空间。
带有下面标志表示不共享:
CLONE_NEWUTS,CLONE_NEWIPC,CLONE_NEWUSER
CLONE_NEWPID,CLONE_NEWNET
2. unshare系统调用:用于取消共享命名空间,创建新的命名空间,起隔离作用。
unshare(CLONE_NEWNET);
容器(container):利用内核中的cgroup和命名空间来实现虚拟化。
cgroup:Control Group
将一组进程放入一个cgroup,并为该cgroup分配资源限制,如CPU、内存、磁盘 I/O 、网络带宽等资源。
适用于容器化、虚拟化、多租户环境。
cgroups有几个子系统:
内存,CPU,net,ns,磁盘I/O
限制CPU资源配置举例:
1. 创建一个名为"my_cgroup"的Cgroup
# sudo mkdir /sys/fs/cgroup/cpu/my_cgroup
2. 将CPU配额设置为50毫秒,周期设置为100毫秒
quota配额:即一个周期内,cgroup中的进程能够使用CPU的总时间量。
# echo 100000 > /sys/fs/cgroup/cpu/my_cgroup/cpu.cfs_period_us
# echo 50000 > /sys/fs/cgroup/cpu/my_cgroup/cpu.cfs_quota_us
3. 将指定进程移到该Cgroup中
# echo > /sys/fs/cgroup/cpu/my_cgroup/tasks
2.3.3 进程ID号
PID 0:swapper或idle进程
PID 1:init 进程,其他进程都由init进程fork而来。
在内核中,一个进程和线程都用task_struct表示。
getpid()和ps命令其实返回的是该进程task_struct中tgid成员,而不是pid成员
tgid:thread group id,线程组ID,即线程组中组长的线程id。
一个进程的所有线程有各自task_struct,但线程组ID(TGID)一样。
单进程(没有线程)时task_struct 中tgid和pid相等。
一个线程的task_struct->group_leader = task_struct (组长线程)
int setpgrp(void);
将调用进程设置为新进程组的组长。
pid_t setsid(void);
创建新会话,并将调用进程设置为该会话组的组长(session leader)。
作用:创建守护进程时,setsid可脱离当前终端会话的控制,在后台独立运行。
同一会话组中所有进程的sid相同。
TGID:线程组ID,直接保存在task_struct中,即task_struct中tgid成员。
会话组和进程组ID:不直接包含在task_struct中,而但保存在信号处理结构中。
进程组ID:task_struct->signal->_pgrp
会话组ID:task_struct->signal->session
同一进程在不同命名空间中PID不一样:
全局ID:init命名空间和内核可见。
局部ID:属于某个特定的命名空间。
struct task_struct {
pid_t pid; //全局PID
pid_t tgid; //全局tgid= thread group id
struct nsproxy *nsproxy; //创建进程时默认是init_nsproxy命名空间
struct pid_link pids[PIDTYPE_MAX]; //有三个链表,PID/PGID/SID
}
1. struct nsproxy init_nsproxy 全局默认命名空间
struct nsproxy init_nsproxy = {
.....
.pid_ns = &init_pid_ns, //struct pid_namespace init_pid_ns;
.....
};
struct pid_namespace
{
struct pidmap pidmap[PIDMAP_ENTRIES]; 位图,表示pid是否已分配
struct task_struct *child_reaper; //当前命名空间的init进程,即用于wait孤儿进程的进程
unsigned int level; 所处第几层命名空间,如父命名空间=0,子level=1,孙level=2
struct pid_namespace *parent; 父命名空间
}
2. struct pid_link
task_struct中包含:
struct pid_link pids[PIDTYPE_MAX]; //三个数组,分别对应PID/PGID/SID
struct pid_link
{
struct hlist_node node; //用于将结构体链接到PID哈希链表,全局pid_hash[]
struct pid *pid; //该指针直接指向pid
};
为了支持pid命名空间,内核增加了pid和upid结构体,upid中nr即是pid
struct pid //内核对PID的内部表示,无需命名空间
{
atomic_t count; //该结构体的引用计数,同一个pid结构可被多个进程共享
unsigned int level; //pid所在的层级
struct hlist_head tasks[PIDTYPE_MAX]; //连接同一ID的所有task_struct,如同一进程的多个线程
struct upid numbers[1]; //内核视图下,子/孙/曾孙命名空间都对应一个upid结构体
};
struct upid { //特定子命名空间中可见,需要指定命名空间
int nr; //在该命名空间中pid(局部ID)
struct pid_namespace *ns;
struct hlist_node pid_chain; 连接到全局pid_hash散列表
};
总结图:
全局pid_hash,哈希表数组。
作用:通过PID值找到对应struct pid,再通过struct pid最后找到task_struct。
如上图可知,在不同level时,pid可以相同。
PID分配器(pid allocator)
bitmap位图:用于检测某pid是否已分配
struct pid *task_pid(struct task_struct *task)
{
return task->pids[PIDTYPE_PID].pid;
}
获取线程组的pid结构:
task->group_leader->pids[PIDTYPE_PID].pid;
如何根据pid结构体来获取数字id
pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
struct upid *upid;
pid_t nr = 0;
if (ns->level level) { 跟踪该struct pid 更顶层命令空间对应pid,所以pid->level更大。
upid = &pid->numbers[ns->level];
if (upid->ns == ns)
nr = upid->nr;
}
return nr;
}
alloc_pidmap:分配一个新PID位图,用于跟踪哪些pid空闲。
2.3.4 进程关系
父进程通过children成员连接到子进程。
子进程之间通过sibling成员连接。
2.4 进程管理相关的系统调用
2.4.1 进程复制
1. 写时复制:COW,copy-on-write
fork vfork clone都最终调用 _do_fork
vfork(由于fork使用了COW技术,vfork优势不再,使用少)
clone(通过CLONE_XX标志精确控制共享哪些资源)
fork子进程时,使用COW机制,原理:
不会复制父进程的整个地址空间。而是将父进程的地址空间标记为只读,并与子进程共享相同的物理内存页。
当父进程或子进程有写内存时,发生缺页异常。
异常处理中检查该页是否可以写,若可以写则复制修改的内存页再修改子进程页表项。若不可以段错误。
COW页:延迟了内存页的复制,直到有写内存需求。
2. 执行系统调用
long do_fork(clone_flags, stack_start, stack_size, int __user *parent_tidptr, int __user *child_tidptr)
stack_start:用户栈
parent_tidptr,child_tidptr:
用于返回父子进程线程ID给用户空间,因为pthread_create函数需要tid值
系统调用在用户空间和内核空间传递参数的方法因体系结构而异:
寄存器传递:速度块,但寄存器数量有限。
栈传递:可传递内容多。
3. do_fork的实现
copy_process:见下节
wake_up_new_task:将该新进程加入调度器队列。
4. copy_process 复制进程
dup_task_struct:
复制父进程task_struct和thread_info结构体
复制后,父子进程两个的task_struct结构体只有一个成员不同:
新进程分配了一个自己的内核栈,即task_struct->stack
thread_info:存储重要线程信息(不同的体系架构,包含信息不同)
通常包含:内核栈栈顶,前线程的task_struct等
union thread_union {
struct thread_info thread_info; 定义在不同体系中
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
THREAD_SIZE=8K,即上图内核栈最大为8K,恶意操作内核栈可能覆盖thread_info
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 = Thread Info Flag
TIF_SIGPENDING 当前进程是否有待决信号
TIF_NEED_RESCHED 当前进程想让出CPU,调度器选择其他进程执行。
如何访问指定线程的thread_info?
(struct thread_info *) (task)->stack
如何访问当前线程的thread_info?
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内核栈的起始地址。
CPU sp寄存器:
当前CPU运行线程的内核栈栈顶。
两者不指向同一地址。
进程切换时,关于进程的task_struct的stack成员,sp寄存器,变化过程?
1. 保存当前进程的上下文:
保存当前进程上下文到内核栈中:包括CPU的通用寄存器、程序计数器PC、栈指SP等。
2. 切换新进程的:
切换到新进程的task_struct结构体,其中task_struct->stack内核栈的起始地址。
3. 切换栈指针SP寄存器
更新SP寄存器为新进程的内核栈的栈顶。以便正确访问新进程的内核栈。
4. 恢复新进程上下文
恢复新进程的内核栈中保存的寄存器信息。
kstack_end(void *addr)函数:
返回当前线程的内核栈的结束地址。
这样就可判断某个地址是否在内核栈区间。
继续回到copy_process
sched_fork函数:
1. 初始化子进程调度参数:优先级和调度策略等。
2. 复制父进程的调度器相关数据(调度器类别,时间片)。
3. 将子进程加入调度队列。
copy_process会检测如下标志:
CLONE_FS 共享父进程的文件系统
CLONE_NEWXX 不共享的资源
CLONE_FILES 共享父进程的文件描述符
CLONE_SIGHAND 共享父进程的信号处理函数
CLONE_MM COW,只复制页表
struct pt_regs {
long uregs[18]; 体系相关,保存当前线程的寄存器状态信息,进程切换后可恢复到CPU
};
存储的寄存器信息有:
#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个寄存器,保存在当前线程的内核栈的底部,如上图。
即 :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进程
内核线程的任务通常是周期任务,如:
回写mmap映射区域到块设备 (有对应文件的VMA,如text,data区域)
回写内存页到交换区(匿名映射:没有对应文件的VMA,如bss,堆栈)
创建内核线程:
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,该特性叫惰性TLB,因为内核线程运行后调度的进程通常还是之前那个用户进程,通过active_mm直接恢复,不用修改映射表,TLB中缓存的映射表仍然有效。
TASK_SIZE:即用户态虚拟地址大小(32位,0-3G)。
内核线程地址空间大于TASK_SIZE。
struct pt_regs *regs:这包含了当前线程执行时的CPU寄存器内容,进程切换时存储和恢复。
存储在当前线程的内核栈最底部中。
ARM为例:
struct pt_regs {
unsigned long uregs[18];
};
struct pt_regs *regs = current_pt_regs(); //从内核栈读取。
uregs 数组包含了 18 个 CPU 寄存器的值,分别是:
r0 - r12: 通用寄存器
sp: 栈指针
lr: 链接寄存器
pc: 程序计数器
cpsr: 当前程序状态寄存器
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
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,释放资源。