1. Linux内核学习之进程和线程初探

1 进程

进程指的是处于执行期的程序。但是需要注意的是进程并不仅仅包括一段可执行程序的代码,它同时还包括其他资源,例如打开的文件,挂起的信号,内核内部数据,处理器状态,具有内存映射的地址空间和执行线程以及数据段等。

1.1 进程描述符

一个操作系统如果想管理好进程,那么操作系统就需要这个进程的所有信息,Linux内核成功抽象了进程这一概念,然后使用task_struct即进程描述符来对进程进行管理,同时内核使用双向链表(即任务队列)对进程描述符进行了相应的组织。(task_struct结构体定义在<linux/sched.h>)。

task_struct和任务队列

task_struct在32位计算机中占有1.7KB。包含一个进程所有的信息,包含打开的文件,进程地址空间,挂起的信号,进程状态等,具体可以参照在Linux内核代码中定义的task_struct结构体代码。Linux在分配进程描述符时,使用了slab机制(可以查看进程创建一节)。当进程描述符task_struct分配完毕之后,需要对其进行存放。

1.2 内核进程操作

对于一个进程来说,在内存中会分配一段内存空间,一般来说这个空间为1或者2个页,这个内存空间就是进程的内核栈。在进程内核栈的栈底有一个结构体变量为thread_info,这个结构体变量中包含了一个指向该进程描述符task_struct的指针,这个变量的存在,可以使内核快速地获得某一个进程的进程描述符,从而提高响应速度。在x86体系结构中,内核中的current宏就是通过对于这个结构体的访问来实现的,而在其他寄存器丰富的体系结构中看,可能会没有使用thread_info结构体,而是直接使用某一个寄存器来完成例如PPC体系结构。

/*x86中thread_info的定义*/
struct thread_info {
    struct task_struct  *task;      /* main task structure */
    struct exec_domain  *exec_domain;   /* execution domain */
    unsigned long       flags;      /* low level flags */
    unsigned long       status;     /* thread-synchronous flags */
    __u32           cpu;        /* current CPU */
    int         preempt_count;  /* 0 => preemptable, <0 => BUG */

    mm_segment_t        addr_limit; /* thread address space:
                         * 0-0xBFFFFFFF for user-thead
                         * 0-0xFFFFFFFF for kernel-thread
                         */
    struct restart_block    restart_block;
    __u8            supervisor_stack[0];
};

1.3 进程PID

Linux的内核使用PID来对进程进行唯一标识。PID是pid_t的隐含类型,PID的值受到<linux/threads.h>头文件中规定的最大值的限制,但是为了和传统的Unix操作系统兼容,PID会被默认设置为32768即short int短整型的最大值。PID的最大值是系统中允许同时存在的进程的最大数目。PID 的最大值可以通过/proc/sys/kernel/pid_max来修改。

1.4 进程家族树

Linux和Unix系统一样,进程之间存在明显的继承关系。所有的进程都是PID为1的init进程的后代。内核会在系统启动的最后阶段启动init进程,这个进程回去读取并且执行系统的初始化脚本(initscript)执行相关程序,完成整个系统的启动。
在Linux操作系统中,每个进程都会有父进程,每个进程都会有0到n个子进程。同一个父进程的所有进程被称为兄弟。进程描述符中,包含了指向父进程的指针,还包含了一个children子进程链表(init进程的进程描述符是静态分配的)。所以通过简单的遍历就可访问到系统中的所有进程。在代码中特意提供了for_each_process(task)宏来进行对整个进程队列(或称任务队列)的访问能力。

2 进程创建

2.1 创建过程

在Linux进程创建不同于其他操作系统,Linux操作系统提供了两个单独的函数完成进程的创建工作。其中fork()函数通过拷贝完成子进程的创建,子进程会完全拷贝父进程中的绝大多数资源,(除了PID和PPID,以及一些敏感资源和统计量)。然后在使用exec()函数完成可执行文件的读取,并且将其载入地址空间运行。而其他操作系统一般只使用一个函数完成上述的两步操作。

fork()函数是通过clone()系统调用实现的。此调用会通过一系列参数标志指明父子进程需要共享的资源。库函数根据参数标志调用clone()clone()调用do_fork()函数do_fork()函数在kernel/fork.c中定义,并且完成了创建中的大部分工作。然后该函数会去调用copy_process()函数copy_process()函数完成了下述工作:
1) 调用duo_task_struct()函数为新进程创建内核栈thread_info、和task_struct,但是这些值都和当前进程的相同,只是一份简单的复制
2) 检查当前用户的进程总数是否超过限制
3) 将进程描述符中关于目前进程的统计信息清零,使得子进程和父进程能够进行区分
4) 将子进程状态设为TASK_UNINTERRUPTIBLE,使其不能运行
5) 调用copy_flag()函数更新task_structflags成员。将超级用户权限标志符PF_SUPERPRIV清零,然后将进程未调用exec()函数标志位PF_FORKNOEXEC置位。
6) 调用alloc_pid()为新进程分配一个有效PID
7) 根据传递给clone()的参数标志,该函数(即copy_process()函数)拷贝或者共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间。
8) 扫尾,然后返回一个指向子进程的指针

当然还有其他形式的fork()函数实现方式。例如vfork()函数功能和fork()函数相同,但是vfork()函数不会拷贝父进程的页表项。vfork()生成的子进程作为一个单独的线程在其地址空间内运行,父进程会被阻塞,直到子进程退出或者调用exec()函数,子进程不允许向地址空间内写入数据。但是在使用了写时拷贝技术之后,这一项技术其实已经无关紧要了。

2.2 进程创建优化

由于进程描述符task_struct是一个在进程创建时必须的数据结构,所以进程的创建速度可以通过加速进程描述符的创建来提高,有鉴于此,内核使用了slab机制来对其进行处理。所谓slab机制,就是对于频繁被使用的数据结构,会进行缓存,而不是使用完毕之后直接进行释放。这样做的好处是,如果需要频繁创建某一数据结构变量,只是直接使用即可,而不需要进行内存的申请,使用完毕也不需要释放,大大减少了分配内存和回收内存的时间。使用slab机制后,进程描述符可以被快速地建立,同时进程销毁时也不需要去进行进程描述符的内存释放。

当然Linux内核在其他方面也使用了加快进程创建的方法。上面讲到,Linux创建进程使用fork()函数来完成,而fork()函数又使用clone()系统调用来实现,但是需要注意的是,创建一个新进程时,Linux内核加入了写时拷贝机制来加速进程的创建,而不是完整地对进程所有内容进行简单的复制。所谓写时拷贝就是在新进程创建时,子进程和父进程共享一个进程地址空间拷贝,当子进程或者父进程对这个拷贝执行写入操作后,数据才会被复制,然后进行各自的修改,所以资源在未进行写入时,以只读方式共享。这种写时拷贝的方式,将进程的创建开销从子进程对父进程资源的大量复制,简化为复制父进程的页表和子进程唯一进程描述符的创建

2.3 进程终结

进程终结时,内核必须要释放他所占用的资源,然后通知父进程。进程的析构发生在exit()系统调用时,可以是显式的,也可以是隐式的,例如从某个程序的主函数返回(对于C语言来说其实会在main()函数的返回点后面设置exit()代码)。当进程收到不能处理但是又不能忽视的信号或者出现异常时,也可能会被动终结。但是进程在终结是,大部分还是会调用do_exit()完成(在kernel/exit.c中定义)。
(1) 将task_struct中的标志成员设置为PF_EXITING
(2) 调用del_timer_sync()删除任意内核定时器。根据返回的结果确认没有任何定时器在排队,同时也没有任何定时器处理程序在运行。
(3) 若开启了BSD的进程记账功能,那么还需要调用acct_update_integrals()来输出记账信息
(4) 调用exit_mm()释放进程占用的mm_struct,若是没有其他进程使用这个地址空间,那么就彻底释放此地址空间
(5) 调用sem_exit()函数,若进程排队等待IPC信号,则离开队列
(6) 调用exit_file()exit_fs(),分别递减文件描述符、文件系统数据的引用计数。若释放后引用计数为0,则直接释放。
(7) 将存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供的任务退出代码,或者完成任何其他由内核机制规定的退出动作。退出代码的存放是为了供父进程检索
(8) 调用exit_notify()函数向自己的父进程发送信号,并且给自己的子进程重新寻找养父,养父为线程组中的其他线程或者为init进程,然后将进程状态置为EXIT_ZOMBLE
(9) do_exit()调用schedule()切换到新进程。这是do_exit()执行的最后代码,退出后就不再返回。

2.3.1 删除进程描述符

进程在执行完do_exit()函数调用之后,会处于EXIT_ZOMBIE退出状态,其所占有的内存就是内核栈thread_info结构task_struct结构体。处于这个状态的进程唯一目的就是向父进程提供信息。父进程检索到信息或者通知内核那是无关的信息后,由进程所持有的剩余的内存释放。

调用do_exit()之后,虽然线程已经僵死不再运行,但是系统还保留了它的进程描述符。这样做可以使系统能够在子进程终结后仍获取其信息。所以进程的终结清理操作可以和进程描述符的删除操作分开运行。
在删除进程描述符的时候,会调用release_task(),完成以下操作:
(1)调用__exit_signal(),由次函数调用_unhash_process(),后者又调用detach_pid()pidhash上删除该进程,同时从任列表中删除该进程
2)__exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终的统计和记录。
3)如果这个进程是进程组最后一个进程,并且领头进程已经死掉,那么release_task()通知僵死的领头进程的父进程
4)调用put_task_struct()释放进程内核栈thread_info结构所占用的页,释放task_struct所占的slab高速缓存

若父进程在子进程之前退出,则首先会为子进程在当前进程组内宣召一个进程作为父亲,若不行,就让init进程作为父进程。

3 线程

线程是指在进程中活动的对象,相对而言,线程仅仅局限在进程内部,线程拥有的资源远远比进程小,仅仅包括独立的程序计数器和进程栈以及一组进程寄存器。在其他操作系统中进程和线程的概念往往会被严格区分,但是对于Linux操作系统内核而言,它对线程和进程并不进行区分,线程通常被视为一个与其他进程共享某些资源的进程。每个线程都拥有自己的task_struct,所以线程在Linux内核中也被视为一个进程,这是和其他操作系统截然不同的。
线程的创建和进程是类似的但是在调用clone()的时候,会传递一些特殊的标志位,例如CLONE_VMCLONE_FSCLONE_FILESCLONE_SIGHAND,这些值都是由下表定义的。


clone()参数标志

内核很多时候还需要在后台执行一些操作,这些都是由内核线程(kernel thread)完成。内核线程独立于内核进程运行,同时内核线程没有独立的地址空间,并且不会切换到用户空间,其他和普通线程一样,没有区别。
内核线程一般是自动从内核进程中衍生而出,同样内核线程也是通过clone()系统调用实现,并且需要调用wake_up_process()函数来进行明确地唤醒。kthread_run()可以完成线程的唤醒和运行,但是本质上只是调用了kthread_create()wake_up_process()。内核线程可以使用do_exit()函数退出,也可以由内核其他部分调用kthread_stop()函数来进行退出。

4 进程和线程的区别

对于Linux内核而言,进程和线程没有区别。对于Linux内核而言,并没有对线程进行特殊处理,而是将线程与进程同等对待,这与其他操作系统完全不同。其他操作系统都提供了专门的机制去实现多线程机制,由于Linux强大轻便快速的进程创建手段,所以Linux仅仅将线程看作是进程共享了进程资源的多个进程,对于Linux内核来说创建线程等价于创建一个进程。通过Linux内核可以得知,一个进程的多线程其实只是共享了很多资源,例如地址空间等。由此产生了“Linux没有多线程机制“”这一说法,但是本质上来说,并不是Linux没有多线程机制,只是其实现方式和其他操作系统不同而已。

这是个人在阅读《Linux内核设计与实现》时候的一点心得,里面加入了一些自己关于操作系统的理解,对自己的现有的知识进行梳理,如有错误敬请指正。

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

推荐阅读更多精彩内容

  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,083评论 0 23
  • Linux 进程管理与程序开发 进程是Linux事务管理的基本单元,所有的进程均拥有自己独立的处理环境和系统资源,...
    JamesPeng阅读 2,442评论 1 14
  • 1,Linux中的进程管理1)进程和线程进程是资源分配的最小单位。线程是操作系统调度执行的最小单位。进程和线程是程...
    SDY_0656阅读 463评论 0 0
  • 这节课来讲解下,油漆桶工具箱中专门针对3D材质选区的一个工具“3D材质拖放工具”的使用方法和技巧。 选择工具箱中的...
    e5a2ee3bb269阅读 1,399评论 0 1
  • 论文交上,补考过了,大学就毕业了,没有大学四级证的四年,没有计算机二级证的四年,没有普通话证,没有奖学金 ...
    寒眼阅读 191评论 0 0