美文网首页
linux进程、线程及调度算法(二)

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

作者: 404Not_Found | 来源:发表于2021-05-19 07:54 被阅读0次
    • 作者: 雪山肥鱼
    • 时间: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 <sched.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    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 <stdio.h>
    #include <sched.h>
    #include <unistd.h>
    
    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 <stdio.h>
    #include <pthread.h>
    #include <linux/unistd.h>
    #include <sys/syscall.h>
    
    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 <stdio.h>
    #include <sys/wait.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    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进程、线程及调度算法(二)

          本文链接:https://www.haomeiwen.com/subject/tvcvjltx.html