- 作者: 雪山肥鱼
- 时间:20210520 07:09
- 目的:进程生命周期,进程的各种状态
# fork
## 内存的重新分配:COW
## vfork
# 线程的引入
##人妖临界态
# PID 和 TGID
# SubReaper 与 托孤
# 再谈睡眠
# 0进程与IDLE进程
fork
执行一个 copy,但是只要任何修改,都造成分裂如,修改了chroot,写memory,mmap,sigaction 等。
p1 是一个 task_struct, p2 也是一个 task_struct. linux内核的调度器只认得task_struck (不管你是进程还是线程), 对其进行调度。
p2 的task_struck 被创建出来后,也有一份自己的资源。但是这些资源会短暂的与p1 相同。
进程是区分资源的单位,你的资源是我的资源,那从概念上将就不叫进程。
- p1 创建了 p2, p2 不可能直接飞到其他地方,刚创建时 与 p1 相同
- 基于上述对进程的概念理解,只要谁动资源 就要分裂成新的进程。
2.1 比如 改变了根路径
2.2 信号重新绑定
2.3 文件资源 p1 有 1、2、3. 文件资源p2 刚开始的时候也有1、2、3. 但随后又打开了4,那么导致分裂。
其他资源都好分配,唯一比较难的是内存资源的重新分配。
关于内存的重新分配 : Copy - on Write COW
#include
#include
#include
#include
int data = 10;
int child_process()
{
printf("child process %d, data %d\n", getpid(), data);
data = 20;
printf("child process %d, data %d\n", getpid(), data);
_exit(0);
}
int main(int argc, char **argv) {
int pid;
pid = fokr();
if(pid == 0) {
child_process();
} else {
sleep(1);
printf("parent process %d, data %d\n", getpid(), data);
exit(0);
}
return 0;
}
非常简单的程序,但是可以充分说明 COW。
结果:10 -> 20 -> 10
- 刚开始只有 P1, 可以看到MMU 中 页表的一项 。这段地址属性是R+2
- 调用fork 后,分裂出的 P2 虚拟地址和物理地址完全相同,但属性从R+W 变为了 RD-Only
- P2 去修改 地址段内容,因为属性不对,直接触发 Page fault
- 将内容修改,但P2 中 与 P1 的virtual 地址相同 的 虚拟地址 virt1所指向的 phy2 变了,即指向的物理地址与P1不用了。
- P1 P2 virt1 在页表中的 属性修改回 R+W
vfork
COW 是严重依赖于CPU中的MMU。CPU如果没有 MMU,fork 是不能工作的。
在没有mmu的CPU中,不可能执行COW 的,所以只有vfork
vfork与fork相比的不同
- 父进程阻塞,直到子进程执行
- exit
- exec
-
p1 将除了 mm 的部分对拷给P2, P2的task_struct 也指向P1的 mm.
P2没有自己的 task_struct, 也就是说P1 的内存资源 就是 P2的内存资源。
#include
#include
#include
int data = 10;
int child_process()
{
printf("child process %d, data %d\n", getpid(), data);
data = 20;
printf("child process %d, data %d\n", getpid(), data);
_exit(0);
}
int main(int argc, char ** argv) {
if(vfork() == 0) {
child_process();
} else {
sleep(1);
printf("Parent process %d, data :%d\n", getpid(),data);
}
return 0;
}
结果 10,20,20
线程引入
vfork:
- CLONE_VM
- CLONE_VFORK
- SIGCHLD
vfork 执行上述流程,P2也只是指向了P1的mm,那么将这个vfork 放大,其余的也全部clone,共同指向P1,那么就是线程的属性了。
phtread_create -> Clone()
- CLONE_VM
- CLONE_FS
- CLONE_FILES
- CLONE_SIGHAND
- CLONE_THREAD
P1 P2 在内核中都是 task_struct. 都可以被调度。共享资源可调度,即线程。这就是线程为什么也叫做轻量级进程
不需要太纠结线程和进程的区别。
人妖临界态
调用clone的适合,也可以指定clone哪一部分。
是一种介于进程和线程之间。
既非进程也非线程
task_struct之间的关系
- 不共享 进程
- 共享 线程
- 部分共享 人妖
PID 和 TGID
- TGID
pthread_create 创出的线程,也是独立task_struct,那么在内核也必定会有自己的pid。但是posix标准要求,多线程,必须向上面看起来像一个整体。也就是说n个线程 同时getpid(),得到的是同一个值。即TGID。
#include
#include
#include
#include
static pid_t gettid(void) {
return syscall(__NR_gettid);
};
static void * thread_fun(void * param) {
printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(), pthread_self());
while(1);
return NULL;
}
int main(void) {
pthread_t tid1, ttid2;
int ret;
printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(), pthread_self());
ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if(ret == -1) {
perror("can not create new thread1");
return -1;
}
ret = pthread_create(&tid2, NULL, thread_fun, NULL);
if(ret == -1) {
perror("can not create new thread2");
return -1;
}
if(pthread_join(tid1, NULL) != 0 ) {
perror("call thread_join function fail");
return -1;
}
if(pthread_join(tid2, NULL) != 0 ) {
perror("call thread_join function fail");
return -1;
}
}
4651 : TGID
4652, 4653 tid 内核中 task_struct 真正的pid
- 所有线程在内核里有自己的pid(tid), 但是共享一个tgid
- top 看到 tgid, top -h 看到内核中真正的pid(tid)
SubReaper 与 托孤
linux 总是白发人 送 黑发人。如果父进程在子进程推出前挂掉了。那么子进程应该怎么办?
p3 -> init, p5 -> subreaper
- 托付给最近一级的 subreaper PR_SET_CHILD_SUBREAPER
- 托付给 init 进程
每一个孤儿都会找最近的火葬场
可以设置进程的属性,将其变为subreaper,会像1号进程那样收养孤儿进程。
#include
#include
#include
#include
int main(void)
{
pid_t pid,wait_pid;
int status;
pid = fork();
if (pid==-1) {
perror("Cannot create new process");
exit(1);
} else if (pid==0) {
printf("child process id: %ld\n", (long) getpid());
pause();
_exit(0);
} else {
printf("parent process id: %ld\n", (long) getpid());
wait_pid=waitpid(pid, &status, WUNTRACED | WCONTINUED);
if (wait_pid == -1) {
perror("cannot using waitpid function");
exit(1);
}
if(WIFSIGNALED(status))
printf("child process is killed by signal %d\n", WTERMSIG(status));
exit(0);
}
}
再谈睡眠
linux的进程睡眠依靠等待队列,这样的机制类似与涉及模式中的订阅与发布。
睡眠,分两种
- 深度睡眠
等待资源的时候 信号不可以被唤醒,比如运行代码的时候 遇到 page fault - 浅睡眠
等待资源的时候,可以被信号唤醒
- 依次挨个唤醒 等待队列,方案繁琐。
- 将所有等待资源的进程挂在等待队列上,资源到位 唤醒等待队列。
- 因为所有进程已经订阅了这个等待队列,所以等待队列唤醒后,所有进程会得到资源, 被唤醒
//没资源,加到一个r_wait的等待队列
add_wait_queue(&dev->r_wait, &wait);
while(dev->current_len == 0 ) {
//读不到,如果是非阻塞,则滚出循环
if( filp -> f_flags & O_NONBLOCK ) {
ret = EAGAIN;
goto out;
}
//进程阻塞,设置为可被打断,
__set_current_state(TASK_INTERRUPTIBLE);
mutex_unlock(&dev->mutex);
//放弃cpu,不能死等,多耗电呀
schedule();
//判断是否被信号唤醒,如果是则goto滚出去。如果不是则有资源,继续往下进行
if(signal_pending(current)) {
ret = ERESTARTSYS;
goto out2;
}
mutex_lock(&dev->mutex);
}
- 一个进程等资源从而进入睡眠,是自己让自己睡眠,自己主动放弃CPU的
- 停止,是进程跑的好好的,被强行中断,当头一棒,被打晕
- linux内核发现你在等东西,帮你置成睡眠
0进程与IDLE进程
每一个进程都是创建出来的,那么第一个进程是谁创建的呢?
init 进程是被linux的 0 进程创建出来的。开机创建。
cd /proc/1
cat status
父进程就是 0 号进程,但在pstree,是看不到0进程的。因为0进程创建子进程后,就退化成了idle进程。
idle进程是 linux内核里,特殊调度类。所有进程都睡眠停止,则调度idle进程,进入到 wait for interrupte 等中断。此时 cpu及其省电,除非来一个中断,才能再次被唤醒。
唤醒后的任何进程,从调度的角度上说,都比idle进程地位高。idle是调度级别最最低的进程。
0 进程 一跑,则进入等中断。一旦其他进程被唤醒,就轮不到 0进程了。
所有进程都睡了,0就上来,则cpu需要进入省电模式