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 <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需要进入省电模式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容