Linux内核设计与实现——读书笔记(1)内核特点、进程和线程

1、内核开发的特点

1.1、内核和应用编程的差异

  主要差异包括以下几种:

(1)、内核编程时既不能访问C库也不能访问标准的C头文件。(不过大多数C库函数在内核中已经实现<linux/xxx.h>)
(2)、内核编程时必须使用GUN C。
(3)、内核编程时缺乏像用户空间那样的内存保护机制。
(4)、内核编程时难以执行浮点运算。
(5)、内核给每个进程只有一个很小的定长堆栈。
(6)、由于内核支持异步中断、抢占和SMP(对称多处理"Symmetrical Multi-Processing"简称SMP,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构),因此必须时刻注意同步和并发。
(7)、要考虑可移植性的重要性。

1.2、内联函数(inline)

  内联函数会在它所调用的位置展开,这样可以消除函数在调用和返回时候的开销。而这意味着占用更多的内存空间和指令缓存。
  通常,只有那些对时间要求比较高,长度比较短的函数才被定义为内联函数
  内联函数必须在使用之前就定义好,不然编译器无法进行展开,所以一般会会被定义在头文件内部,并且用static限定,使其不会展开为函数体。如果一个内联函数仅仅在某个源文件中使用,那么可以将这个内联函数定义在原文件的开头位置。

static inline void xxx(void)

1.3、内联汇编

  linux内核混合使用了C和汇编语言,在较为底层或者对时间要求比较高的地方通常使用汇编来编写。我们通常使用asm()函数来内嵌汇编指令,内联汇编的格式等参考linux 源码中__asm__ __volatile__作用.

1.4、条件语句的优化

  在内核中用unlikely()和linkely()函数时,编译器可以对条件语句进行优化,例子如下:

/* 绝少数发生的话,认为error大多时候都为0 */
if(unlikely(error)){
	...
}
/* 大多数数发生的,认为error大多时候都为真 */
if(likely(error)){
	...
}

  使用这两个语句进行优化前,一定要明确知道是否绝对发生或者不发生。否则,性能反而回下降。

1.5、没有内存保护机制

  内核中非法访问不会报错,可能会整个挂掉。内核的内存不分页,用一点物理内存就少一点。

1.6、不要随便使用浮点数操作

  内核不能完美支持浮点数操作,本身不能陷入。在内核中使用浮点数时,出了要人工保存和恢复浮点寄存器,还有其他乱七八糟的事要做,我也不知道是啥,书里也没讲(或者在后面我还没看到)。总之,不要随便用浮点操作。

1.7、容量小且固定的栈

  通常,内核栈的大小是两页,32位机为8kB,64位机为16kB。

1.8、同步与并发

  内核中很多特性都要求并发地访问共享数据,这就要求有同步机制以保证不出现竞争关系,特别是:

多任务之间,内核的进程调度程序,对进程的调度和重新调度。
在SMP系统中,内核对共享资源的保护。
中断到来的情况下,内核对资源的保护。
内核的互相抢占,几段代码同时访问相同资源时发生的抢占。

  常见的解决竞争的方式是自旋锁和信号量

2、进程管理

2.1、进程

  内核调度的对象实际上是线程,而不是进程。
  在内核中,fork()实际上时使用clone()函数实现的。

2.2、进程描述符及数据结构

  内核把进程的列表存放在叫做任务队列的双向列表中。列表中的每一项都是task_struct
的结构体,这个结构体类型称为 进程描述符 ,该结构体定义在<linux/sched.h>头文件中。
  进程描述符中包含的数据能够完整地描述一个正在执行的程序:

打开的文件;
进程的地址空间;
挂起的信号;
进程的状态等;

2.3、进程描述符的分配

  2.6版本以后的内核通过slab分配器分配task_struct描述符,这样能达到对象复用缓存着色的目的。用slab分配器动态生成task_strcut结构,只需在栈底(向下生长的栈)或者栈顶(向上生长的栈)创建一个新的结构struct thread_info
在这里插入图片描述

2.4、进程描述符的存放

  内核通过一个唯一的进程标识值或者PID来标识每个进程。为了和老版本的UNIX和LINUX兼容,PID的最大默认值为32768(<linux/threads.h>中定义了PID的最大值)。如果不考虑老系统的兼容性,管理员可以直接修改/proc/sys/kernle/pid_max中的数值来提高最大值上限。
  内核会把每个进程的PID放在其进程描述符中。内核中,绝大多数处理进程的代码都是通过task_struct进行的,一般通过current宏来定位task_struct,所以current实现定位的速度就尤为重要。在不同架构的机器上,current宏实现的方法不同,x86上没有专门存放task_struct的寄存器,只能在内核栈的尾端创建task_struct结构,因此只能通过计算偏移值间接获取到task_struct结构。而相对于拥有存放task_struct位置的寄存器来说,只需要返回寄存器中的数据就能找到task_struct。

2.5、设置进程状态

  内核调整某个进程状态使用set_task_state(task,state)函数:

	set_task_state(task,state)				//将task任务设置为state状态

  必要时候,set_task_state(task,state)函数内部的实现会使用内存屏障来强制让SMP系统的其他处理器做重新排序,否则,set_task_state(task,state)函数等价于

	task->state = state;				//task为struct thread_info中task_struct结构的成员,

  set_task_state(task,state) 和set_current_state(state)含义是等同的,见<linux/sched.h>。

2.6、进程家族树

  所有进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行其他相关的程序,最终完成整个系统的启动。
  每个task_struct都包含一个指向其父进程的task_struct,叫做parent指针,还包含一个叫做children的子进程链表。

2.6.1、访问父子进程

  可以直接通过parent指针来访问父进程,可以通过以下方式来一次访问子进程:

	struct task_struct *task;
	struct list_head *list;
	INIT_LIST_HEAD(&list);
	list_for_each(list,&current->children){
		/* 获取子进程的地址 */
		task = list_entry(list,struct task_struct,sibling);
	}

  list链表必须初始化1,如果使用没有初始化的链表可能导致内核异常。
  list_entry宏是调用container_of来获取子进程的首地址。container_of参考链接.

2.6.2、定位init进程

  init进程的进程描述符是作为init_task静态分配的。可以通过以下代码找到Init进程:

	struct task_struct *task;
	for(task = current;task != &init_task;task = task->parent);
	/* 此时task指向init */

2.6.3、访问前后进程

  通过 next_task(task) 宏和 prev_task(task) 宏实现访问前后进程。
  for_each_process(task) 宏提供了依次访问整个任务队列的能力。
  注意:在系统中遍历所有进程的代价很大。

2.7、进程创建机制

  在其他很多操作系统中,使用如下方式产生一个进程:首先在新的地址里创建进程,随后读入可执行程序文件,最后进行执行。
  UNIX将上述步骤分解到:fork()和exec()函数中执行。

2.8、写时拷贝技术

  现在的Linux系统在fork时都使用写时拷贝页实现。只有在需要写入的时候,数据才会被复制,从而各个进程拥有各自的拷贝。在此之前,只是以只读的方式共享。例如,在fork()之后调用exec()运行一个可执行文件,那么就不需要拷贝数据。这样可以很大程度优化进程的执行能力。
  fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程标识符。

2.9、fork()

   Linux通过clone()系统调用实现fork(),clone()系统调用通过一系列参数标志指明父子进程需要共享的资源。类似的,vfork()和__clone()库函数都通过自身需要的参数标志去调用clone(),然后再由clone()去调用do_fork()
  一个普通的fork()的实现是:

	clone(SIGCHLD,0);

   clone()的参数标志以及它们的作用在<linux/sched.h>中定义:

 /*
 * cloning flags:
 */
#define CSIGNAL					0x000000ff  /* signal mask to be sent at exit */
#define CLONE_VM				0x00000100  /* set if VM shared between processes */
#define CLONE_FS				0x00000200  /* set if fs info shared between processes */
#define CLONE_FILES				0x00000400  /* set if open files shared between processes */
#define CLONE_SIGHAND			0x00000800  /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE			0x00002000  /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK				0x00004000  /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT			0x00008000  /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD			0x00010000  /* Same thread group? */
#define CLONE_NEWNS				0x00020000  /* New namespace group? */
#define CLONE_SYSVSEM			0x00040000  /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS			0x00080000  /* create a new TLS for the child */
#define CLONE_PARENT_SETTID 	0x00100000  /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID	0x00200000  /* clear the TID in the child */
#define CLONE_DETACHED      	0x00400000  /* Unused, ignored */
#define CLONE_UNTRACED			0x00800000  /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID		0x01000000  /* set the TID in the child */
#define CLONE_STOPPED			0x02000000  /* Start in stopped state */
#define CLONE_NEWUTS			0x04000000  /* New utsname group? */
#define CLONE_NEWIPC			0x08000000  /* New ipcs */
#define CLONE_NEWUSER			0x10000000  /* New user namespace */
#define CLONE_NEWPID			0x20000000  /* New pid namespace */
#define CLONE_NEWNET			0x40000000  /* New network namespace */

   do_fork完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_process()的函数,然后让进程开始运行。copy_process()函数完成的工作很有意思:

  • (1)调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
  • (2)检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
  • (3)子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或者设为初始值。进程描述符的成员值并不是继承而来的,而主要是统计信息。进程描述符中的大多数数据都是共享的.
  • (4)子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
  • (5)copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV的标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
  • (6)调用alloc_pid()为新进程分配一个有效的PID。
  • (7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到了这里。
  • (8)让父进程和子进程平分剩余的时间片。
  • (9)最后,copy_process()做扫尾工作并返回一个指向子进程的指针。
  • (10)再回到do_fork()函数,如果copy_process()函数返回成功,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。 因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

注意:尽管内核有意选择子进程首先执行,但是并非总能如此。

fork()
------>clone()
------ ------>do_fork()
------ ------ ------>copy_process()
------ ------ ------ ------>dup+task_struct() //创建内核栈、thread_info和task_struct
------ ------ ------ ------>copy_flags() //更新task_struct的flags成员
------ ------ ------ ------>alloc_pid() //分配有效PID

2.10、vfork()

   vfork()调用clone()的实现是:

	clone(CLONE_VFORK | CLONE_VM | SIGCHLD,0);
  • vfork()不拷贝父进程的页表项。
  • vfork()创建的子进程作为父进程的一个单独的线程在它的地址空间运行,父进程被阻塞,直到子进程退出或者执行exec()。如果子进程依赖父进程的进一步操作,则会造成死锁。
  • 通常使用fork()就好了。

3、线程在linux中的实现

  和windows等操作系统不同,Linux线程的实现机制非常独特。从内核的角度看,并没有线程的概念,Linux把所有线程都当做进程来实现,并没有特别的调度算法或者数据结构来描述线程。线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一的属于自己的task_struct。

3.1、创建线程

  线程的创建和普通进程的创建类似,只不过在clone()调用的时候要传递一些参数标志来指明要共享的资源:

	clone(CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND,0)

3.2、内核线程

  内核线程和普通线程的区别在于内核线程没有独立的地址空间(指向地址空间的mm指针被设置为NULL),只运行在内核空间。
  内核线程同普通线程一样,可以被调度和抢占。内核线程也只能由其他内核线程创建,内核是用过kthreadd内核进程来衍生出所有新的内核线程。在<linux/kthread.h>中有创建内核线程的定义。

	struct task_struct *kthread_create(int (*threadfn)(void *data),
	                   void *data,
	                   const char namefmt[], ...);

  新的线程任务是由kthread内核进程通过clone()系统调用创建的,新的线程将运行threadfn函数data为传递给函数的参数,新的线程被命名为namefmtnamefmt可接受类似于printf的格式化参数。
  新创建的线程处于不可运行状态,如果不使用wake_up_process()函数来唤醒它,它不会主动运行。可以通过宏kthread_run()来创建并且运行新的线程。
  内核线程一旦启动就一直运行,直到调用
do_exit()退出
,或者在内核的其他部分调用kthread_stop() 退出,传递给kthread_stop的参数为使用kthread_create()函数返回的task_struct结构的地址。

3.3、进程终结

  进程可以调用exit()系统调用终止,或者main函数的返回(实际上main函数返回的时候也调用了exit()),或者进程接收到既不能处理又不能忽略的信号或异常时,有可能被动地终止。不管进程是怎么终止的,大部分工作都要调用do_exit()来完成,函数定义在kernel/exit.c中。do_exit() 主要完成了以下工作:

  • 1)将task_struct中的标志成员设置为PF_EXITING
  • 2)调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
  • 3)如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息。
  • 4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用他们(也就是说这个地址空间没有被共享),就彻底释放它们。
  • 5)接下来调用sem_exit()函数。如果进程排队等候IPC信号,则进程离开队列。
  • 6)调用exit_files()和exit_fs(),分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用技术的数值降为零,那么就岱庙没有进程在使用相应的资源,可以释放。
  • 7)接着把存放在task_struct中的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成其它由内核机制规定的退出动作。退出代码存放在这里提供给父进程随时检索。
  • 8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把进程状态(task_struct结构中的exit_status)设置为EXIT_ZOMIE。
  • 9)do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程执行的最后一段代码。do_exit()永不返回。
      至此,与进程相关联的资源都被释放掉了,此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。

3.4、删除进程描述符

  在调用do_exit()之后,尽管进程已经僵死不能再运行,但是系统还是保留了它的进程描述符。这样做可以让系统有办法在子进程结束后仍能获得它的信息。因此,进程终止时所需的清理工作和进程描述符的删除被分开执行的。在父进程获取到子进程的信息后,或者通知内核那是无关的信息后,子进程的task_struct结构才被释放。
   wait()这一族函数都是通过唯一的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此函数会返回改子进程的PID。此外,调用改函数时提供的指针会包含子函数退出时的退出代码。
  当最终需要释放进程描述符时,release_task() 会被调用,完成以下工作:

  • 1)调用 __exit_signal() ,该函数调用 _unhash_process(),后者又调用detach_pid()pidhash 上删除该进程,同时也要从任务中删除该进程。
  • 2)_exit_signal() 释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
  • 3)如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么 release_task() 就要通知僵死的领头进程的父进程。
  • 4)release_task() 调用 put_task_struct() 释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存。
      至此,进程描述符和进程独享的资源全部释放完毕。

3.5、孤儿进程造成的进退维谷

  如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些为孤儿的进程会在退出时永远处于僵死状态,损耗内存。
  解决方法:在当前线程组内找一个线程作为父亲,如果没有,让init做父进程。

do_exit()
------>exit_notify()
------ ------>forget_original_parent()
------ ------ ------>find_new_reaper()

  do_exit() 函数最后会调用find_new_reaper() 来执行寻父过程。
  在找到适合的养父后,遍历所有子进程并为他们设置新的父进程:
在这里插入图片描述
  然后调用ptrace_exit_finish() 为ptraced的子进程寻找父亲。
  ptraced是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。ptraced.
  
  
  
  
  
  


  1. 可以使用LIST_HEAD(xx_list);  //定义并初始化xx_list链表。或者,
    struct list_head xx_list;       // 定义一个链表
    INIT_LIST_HEAD(&xx_list);    // 使用INIT_LIST_HEAD函数初始化链表 ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_zhangsq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值