目录
前言
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支持。
实时进程的优先级比普通进程高。
而进程的优先级最终决定了进程运行的时间片比例,和调度先后顺序。
每个进程都有各自内核栈,用于存储该进程在内核态执行的函数参数、局部变量、寄存器等数据。
内核栈大小为THREAD_SIZE,通常为8K。
不同进程的内核栈不会重叠,但都在地址空间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_RRstruct 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 file size unlimited unlimited bytes Max stack size 8388608 unlimited bytes Max open files 1024 4096 files Max pending signals 257417 257417 signals
setrlimit系统调用:
作用:设置该进程的资源限制。如:
最大CPU时间,最大文件长度,数据段最大长度,用户栈最大长度
打开文件最大数目,待决信号最大数目,最大实时优先级,
不可换出页最大数目(mlock)
int mlock(const void *addr, size_t len);
将进程指定的内存区域锁定在物理内存中,防止被内核交换出去,可保证该内存区域访问速度更快。
使用场景:高性能的应用程序,比如实时应用程序、高性能数据库等。
SIGQUEUE_MAX:待决信号队列大小。默认32或64,高性能场景值更大,是系统级参数,对所有进程生效。
2.3.1 进程类型
2.3.2 命名空间
命名空间部分稍显复杂。
命名空间(Namespace)和控制组(Cgroups):
用于实现进程隔离和资源限制。
命名空间可用于容器技术,如Docker
如何创建新命名空间:
1. fork时根据标志是共享命名空间,还是新建命名空间。
带有下面标志表示不共享:
CLONE_NEWUTS,CLONE_NEWIPC,CLONE_NEWUSER
CLONE_NEWPID,CLONE_NEWNET
2. unshare系统调用:用于取消共享命名空间,创建新的命名空间,起隔离作用。
unshare(CLONE_NEWNET);
实现:
struct task_struct { struct nsproxy *nsproxy; 该进程的命名空间。如上图多个进程可共享同一命名空间。}struct nsproxy { atomic_t count;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_nsproxy命令空间。
struct nsproxy init_nsproxy = {
.uts_ns = &init_uts_ns,
...
.pid_ns = &init_pid_ns,
};
所有进程默认pid命名空间是nit_pid_ns,level为0。
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 nodename[65];char release[65];char version[65];char machine[65];
};
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; //全局PIDpid_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
struct task_struct {struct pid_link pids[PIDTYPE_MAX]; //有三个链表,PID/PGID/SID
}
struct pid_link
{struct hlist_node node; //该字段帮助使pid通过链表遍历访问到task_structstruct pid *pid; //该指针直接指向pid
};
为了支持pid命名空间,内核增加了pid和upid结构体,upid中nr即是pid
同一个struct pid可以被不同的stask_struct使用。
struct pid //内核对PID的内部表示,无需命名空间
{atomic_t count; //该结构体的引用计数,同一个pid结构可被多个进程共享unsigned int level; //pid所在的层级struct hlist_head tasks[PIDTYPE_MAX]; //多个链表头,连接同一ID的所有task_structstruct upid numbers[1]; //内核视图下,子/孙/曾孙命名空间都对应一个upid结构体
};
struct upid { //特定子命名空间中可见,需要指定命名空间int nr; //该命名空间中pid(即局部ID)struct pid_namespace *ns; 该pid命名空间struct hlist_node pid_chain; 当hash冲突时,用于连接同一hash表数组所有冲突upid实例
};
总结图:
全局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. _do_fork函数
fork vfork clone都最终调用 _do_fork
clone:通过CLONE_XX标志精确控制共享哪些资源)
vfork:由于fork使用了COW技术,vfork优势不再,使用少。
COW:copy-on-write,写时复制。
fork子进程时,使用COW机制,原理:
不复制父进程的地址空间。而是将父进程的地址空间标记为只读,并与子进程共享相同的物理内存页。
当父进程或子进程有写内存时,发生缺页异常。
异常处理中检查该页是否可以写,若可以写则复制修改的内存页再修改子进程页表项。若不可以段错误。
COW页:避免不必要的拷贝,提高性能。
2. 执行系统调用
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值
系统调用在用户空间和内核空间传递参数的方法因体系结构而异:
寄存器传递:速度块,但寄存器数量有限。
栈传递:可传递内容多。
3. do_fork的实现
copy_process:见下节
wake_up_new_task:将该新进程加入调度器队列。
4. copy_process 复制进程
dup_task_struct:
复制父进程task_struct和thread_info结构体
thread_info:也存储重要线程信息,只是各体系架构定义不一样。从task_struct中独立出来。
通常包含:内核栈栈顶,指向当前线程的task_struct等。
创建新进程时分配了新的内核栈,即task_struct->stack
复制后,父子进程两个的task_struct结构体只有一个成员不同:
新进程分配了一个自己的内核栈,即task_struct->stack
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_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?
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寄存器,变化过程?
1. 保存当前进程的上下文:
保存当前进程上下文到内核栈中:包括CPU的通用寄存器、程序计数器PC、栈指SP等。
2. 切换新进程的:
切换到新进程的task_struct结构体,再通过task_struct->stack得到thread_info。
3. 恢复新进程上下文
从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函数:
1. 初始化子进程调度参数:优先级和调度策略等。
2. 复制父进程的调度器相关数据(调度器类别,时间片)。
3. 将子进程加入调度队列。
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
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,释放资源。