linux进程、线程及调度算法(二)

  • 作者: 雪山肥鱼
  • 时间:20210520 07:09
  • 目的:进程生命周期,进程的各种状态
# fork
  ## 内存的重新分配:COW
  ## vfork
# 线程的引入
  ##人妖临界态
# PID 和 TGID
# SubReaper 与 托孤
# 再谈睡眠
# 0进程与IDLE进程

fork

fork的对拷机制.png

执行一个 copy,但是只要任何修改,都造成分裂如,修改了chroot,写memory,mmap,sigaction 等。

p1 是一个 task_struct, p2 也是一个 task_struct. linux内核的调度器只认得task_struck (不管你是进程还是线程), 对其进行调度。
p2 的task_struck 被创建出来后,也有一份自己的资源。但是这些资源会短暂的与p1 相同。
进程是区分资源的单位,你的资源是我的资源,那从概念上将就不叫进程。

  1. p1 创建了 p2, p2 不可能直接飞到其他地方,刚创建时 与 p1 相同
  2. 基于上述对进程的概念理解,只要谁动资源 就要分裂成新的进程。
    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


COW 解读.png
  1. 刚开始只有 P1, 可以看到MMU 中 页表的一项 。这段地址属性是R+2
  2. 调用fork 后,分裂出的 P2 虚拟地址和物理地址完全相同,但属性从R+W 变为了 RD-Only
  3. P2 去修改 地址段内容,因为属性不对,直接触发 Page fault
  4. 将内容修改,但P2 中 与 P1 的virtual 地址相同 的 虚拟地址 virt1所指向的 phy2 变了,即指向的物理地址与P1不用了。
  5. P1 P2 virt1 在页表中的 属性修改回 R+W

vfork

COW 是严重依赖于CPU中的MMU。CPU如果没有 MMU,fork 是不能工作的。
在没有mmu的CPU中,不可能执行COW 的,所以只有vfork
vfork与fork相比的不同

  • 父进程阻塞,直到子进程执行
  1. exit
  2. exec
  3. p1 将除了 mm 的部分对拷给P2, P2的task_struct 也指向P1的 mm.


    P2指向P1.png

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:

  1. CLONE_VM
  2. CLONE_VFORK
  3. SIGCHLD
图片.png

vfork 执行上述流程,P2也只是指向了P1的mm,那么将这个vfork 放大,其余的也全部clone,共同指向P1,那么就是线程的属性了。
phtread_create -> Clone()

  1. CLONE_VM
  2. CLONE_FS
  3. CLONE_FILES
  4. CLONE_SIGHAND
  5. CLONE_THREAD

P1 P2 在内核中都是 task_struct. 都可以被调度。共享资源可调度,即线程。这就是线程为什么也叫做轻量级进程
不需要太纠结线程和进程的区别。

人妖临界态

只clone一部分.png

调用clone的适合,也可以指定clone哪一部分。
是一种介于进程线程之间。
既非进程也非线程
task_struct之间的关系

  1. 不共享 进程
  2. 共享 线程
  3. 部分共享 人妖

PID 和 TGID

PID 和 TGID.png
  • 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;
  }
}
运行结果.png

4651 : TGID
4652, 4653 tid 内核中 task_struct 真正的pid

  1. 所有线程在内核里有自己的pid(tid), 但是共享一个tgid
  2. top 看到 tgid, top -h 看到内核中真正的pid(tid)

SubReaper 与 托孤

linux 总是白发人 送 黑发人。如果父进程在子进程推出前挂掉了。那么子进程应该怎么办?


图片.png

p3 -> init, p5 -> subreaper

  1. 托付给最近一级的 subreaper PR_SET_CHILD_SUBREAPER
  2. 托付给 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
  • 浅睡眠
    等待资源的时候,可以被信号唤醒
图片.png
  • 依次挨个唤醒 等待队列,方案繁琐。
  • 将所有等待资源的进程挂在等待队列上,资源到位 唤醒等待队列。
    • 因为所有进程已经订阅了这个等待队列,所以等待队列唤醒后,所有进程会得到资源, 被唤醒
//没资源,加到一个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);
}
  1. 一个进程等资源从而进入睡眠,是自己让自己睡眠,自己主动放弃CPU的
  2. 停止,是进程跑的好好的,被强行中断,当头一棒,被打晕
  3. linux内核发现你在等东西,帮你置成睡眠

0进程与IDLE进程

每一个进程都是创建出来的,那么第一个进程是谁创建的呢?
init 进程是被linux的 0 进程创建出来的。开机创建。

cd /proc/1
cat status
proc 1's status.png

父进程就是 0 号进程,但在pstree,是看不到0进程的。因为0进程创建子进程后,就退化成了idle进程。
idle进程是 linux内核里,特殊调度类。所有进程都睡眠停止,则调度idle进程,进入到 wait for interrupte 等中断。此时 cpu及其省电,除非来一个中断,才能再次被唤醒。
唤醒后的任何进程,从调度的角度上说,都比idle进程地位高。idle是调度级别最最低的进程。
0 进程 一跑,则进入等中断。一旦其他进程被唤醒,就轮不到 0进程了。
所有进程都睡了,0就上来,则cpu需要进入省电模式

你可能感兴趣的:(linux进程、线程及调度算法(二))