Chinaunix首页 | 论坛 | 博客
  • 博客访问: 623989
  • 博文数量: 201
  • 博客积分: 3076
  • 博客等级: 中校
  • 技术积分: 2333
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-02 19:44
文章分类

全部博文(201)

文章存档

2010年(118)

2009年(83)

我的朋友

分类: LINUX

2010-01-04 13:02:58

看了3个周末的Linux内核,包括前两周总结的,。当时研究这个的原因是因为。不过到现在相关的背景知识也了解了,问题也早已经解决了,所以暂时也没有进一步的兴趣驱动去研究内核了,这里是终结篇。:)

在应用程序运行的时候,特别是抢占式的操作系统里面,内核怎么样取得控制权?这依赖硬件支持的系统定时器来实现。内核中有非常多的功能由定时器触 发。系统定时器以固定的频率触发,这个频率称为tick rate(HZ),每个触发周期的时间叫做tick, 它等于1/HZ秒。触发后系统会转到内核相应的handler去处理。

在系统定时器中断时候内核需要做以下事情

  • 更新uptime, time of day, resource及处理器使用状态
  • 检查多处理器是否负载均衡
  • 检查当前进程是否timeslice是否用完,如果用完则重新分配下阶段计划(reschedule)
  • 运行应用程序自定义的定时器(dynamic timers)

了解了tick rate之后才能比较好理解为什么sleep(), timer等API声称都不是提供精确的时间,因为cpu一个任务timeslice没运行完,系统是不会马上切换到这些dynamic timer的进程的。

Linux 2.5以上将tick从100改成1000,相当于系统时钟从10ms变成每1ms要中断一次,当时也引起较大的争议。

改成1000的优点有:

  • 系统时钟精度更高更准确。
  • 系统调用如poll(), select() 等依赖系统时钟的调用性能得到很大改善。
  • 内核实时运行指标如资源占用,uptime更准确。
  • 抢占式机制更精确。

缺点:
我们知道定时器频繁被触发必将会增加开销,降低throughput。这是很多人反对改成1000的理由。原理上它在CPU的时间中断上将会增加10倍的 开销。但由于现代CPU硬件速度的变快。大家也逐渐认可了新的调整不会给整体性能带来大的影响。如果在意这一点的可以重新编译内核, 将中HZ的值由1000降低。目前可以接受的值有100,500和1000。

Linux 2.6.21 又增加了一种叫dyntick的技术,dyntick就是在系统空闲的时候,彻底停止时间中断, 避免cpu空转,它会带来节能方面的好处。详情可参看。这是个新的技术,一般的书上都没来得及写进去。

比较感兴趣的后续课题是virtualization系统中的timer实现。

----------------------------------

上周碰到部署在真实服务器上某个应用的问题,虽然经过tuning, 问题貌似已经解决,但我对tuning的方式只是基于大胆的假设并最终生效了。我更希望更多的求证一下程序背后CPU及OS kernel当时的运作机制。所以我读了一些及其他一些相关资料,对Linux process的机制与切换有了更多一些体会。本文尽可能条理一点,但由于牵涉点较多,同时自己可能觉得某些点有记录的价值,因此文字可能会零散。

  • 进程状态

Linux进程的状态比较容易理解,值得注意的是 UNINTERRUPTIBLE 及 ZOMBIE

TASK_RUNNING
TASK_INTERRUPTIBLE
TASK_UNINTERRUPTIBLE 此时进程不接收信号,这就是为什么有时候kill一个繁忙的进程没有响应。
TASK_ZOMBIE 我们经常 kill -9 pid 之后运行ps会发现被kill的进程仍然存在,状态为 zombie。zombie的进程实际上已经结束,占用的资源也已经释放,仅由于kernel的相关进程描述符还未释放。
TASK_STOPPED

  • Kernel space and user space

Kernel space是供内核,设备驱动运行的内存区域。user space是供普通应用程序运行的区域。每一个进程都运行在自己的虚拟内存区域,不能访问其他进程的内存空间。普通进程不能访问kernel space, 只能通过系统调用来间接进行。当系统内存比较紧张时,非当前运行进程user space可能会被swap到磁盘。

使用命令 pmap -x 可以查看进程的内存占用信息; lsof -a -p 可以查看一个进程打开的文件信息。ps -Lf 可以查看进程的线程数。

另外procfs也是一个分析进程结构的好地方。procfs是一个虚拟的文件系统,它把系统中正在运行的进程都显现在/proc/目录下。

  • 进程创建

进程创建通常调用fork实现。创建后子进程和父进程指向同一内存区域,仅当子进程有write发生时候,才会把改动的区域copy到子进程新的地址空间,这就是copy-on-write技术,它极大的提高了创建进程的速度。

  • Linux的线程实现

Linux线程是通过进程来实现。Linux kernel为进程创建提供一个clone()系统调用,clone的参数包括如 CLONE_VM, CLONE_FILES, CLONE_SIGHAND 等。通过clone()的参数,新创建的进程,也称为LWP(Lightweight process)与父进程共享内存空间,文件句柄,信号处理等,从而达到创建线程相同的目的。

Linux 2.6的线程库叫NPTL(Native POSIX Thread Library)。POSIX thread(pthread)是一个编程规范,通过此规范开发的多线程程序具有良好的跨平台特性。尽管是基于进程的实现,但新版的NPTL创建线程的效 率非常高。一些测试显示,基于NPTL的内核创建10万个线程只需要2秒,而没有NPTL支持的内核则需要长达15分钟。

在Linux 2.6之前,Linux kernel并没有真正的thread支持,一些thread library都是在clone()基础上的一些基于user space的封装,因此通常在信号处理、进程调度(每个进程需要一个额外的调度线程)及多线程之间同步共享资源等方面存在一定问题。为了解决这些问题,当 年IBM曾经开发一套NGPT(Next Generation POSIX Threads), 效率比 LinuxThreads有明显改进,但由于NPTL的推出,NGPT也完成了相关的历史使命并停止了开发。

NPTL的实现是在kernel增加了futex(fast userspace mutex)支持用于处理线程之间的sleep与wake。futex是一种高效的对共享资源互斥访问的算法。kernel在里面起仲裁作用,但通常都由进程自行完成。

NPTL是一个1×1的线程模型,即一个线程对于一个操作系统的调度进程,优点是非常简单。而其他一些操作系统比如Solaris则是MxN的,M对应创建的线程数,N对应操作系统可以运行的实体。(N

  • 信号

进程接收信号有两种:同步和异步。同步信号比如SEGILL(非法访问), SIGSEGV(segmentation fault)等。发生此类信号之后,系统会立即转到内核陷阱处理程序,因此同步信号也称为陷阱。异步信号如kill, lwp_kill, sigsend等调用产生的都是,异步信号也称为中断。

kill 调用的是 SIGTERM, 此信号可以被捕获和忽略。

kill -9 调用的是 SIGKILL, 杀掉进程,不能被捕获和忽略。

SIGHUP是在终端被断开时候调用,如果信号没有被处理,进程会终止。这就是为什么突然断网刚通过远程终端启动的进程都终止的原因。防止的方法是在启动的命令前加上 nohup 命令来忽略 SIGHUP信号。如 nohup ./startup.sh &

很多应用程序通常捕获SIGHUP用来实现一些自定义特性,比如通过控制台传递信号让正在运行的程序重新加载配置文件,避免重启带来的停止服务的副 作用。可惜的是,在JAVA中没法直接使用这一功能,SUN JVM没有官方的signal支持,尽管它已经可以实现,详情可参看Singals and Java.

另外有个有趣的现象是 zombie 状态的进程 kill/kill -9 都没有任何作用,这是由于进程本身已经不存在,所以没有相应的进程来处理signal, zombie状态的进程只是kernel中的进程描述符及相关数据结构没有释放,但进程实体已经不存在了。

--------------------------------------

Linux 的僵尸(zombie)进程

可 能很少有人意识到,在一个进程调用了exit之后,该进程 并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构。在Linux进程的5种状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所 有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有 任何内存空间。

僵尸进程的来由,要追溯到Unix,Unix的设计者们设计这个东西并非是因为闲来无事想装装酷什么的。上面说到,僵尸进程中保存着很多对程序员和 系统管理员非常重要的信息,首先,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?也就是说,这个程序的退出码是什么? 其次,这个进程占用的总系统CPU时间和总用户CPU时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵 尸进程,进程执行多长我们并不知道,一旦其退出,所有与之相关的信息都立刻都从系统中清除,而如果此时父进程或系统管理员需要用到,就只好干瞪眼了。

所以,进程退出后,系统会把该进程的状态变成Zombie,然后给上一定的时间等着父进程来收集其退出信息,因为可能父进程正忙于别的事情来不及收集,所以,使用Zombie状态表示进程退出了,正在等待父进程收集信息中。

Zombie进程不可以用kill命令清楚,因为进程已退出,如果需要清除这样的进程,那么需要清除其父进程,或是等很长的时间后被内核清除。因为Zombie的进程还占着个进程ID号呢,这样的进程如果很多的话,不利于系统的进程调度。

下面,让我们来看看一个示例:

01/* zombie.c */
02#include
03#include   main()
04{
05    pid_t pid; 
06    pid=fork();
07    if(pid<0) { /* 如果出错 */ 
08        printf("error occurred!\n");
09    }else if(pid==0){ /* 如果是子进程 */ 
10        exit(0);
11    }else/* 如果是父进程 */ 
12        sleep(60);  /* 休眠60秒 */ 
13        wait(NULL); /* 收集僵尸进程 */
14    }
15}

编译这个程序:

1$ cc zombie.c -o zombie

后台运行程序,以使我们能够执行下一条命令

1$ ./zombie &
2[1] 1217

列一下系统内的进程

1$ ps -ax
2... ...
31137   pts/0   S   0:00   -bash
41217   pts/0   S   0:00   ./zombie
51218   pts/0   Z   0:00   [zombie]
61578   pts/0   R   0:00   ps   -ax

其中的”Z”就是僵尸进程的标志,它表示1218号进程现在就是一个僵尸进程。

收集Zombie进程的信息,并终结这些僵尸进程,需要我们在父进程中使用waitpid调用和wait调用。这两者的作用都是收集僵尸进程留下的信息,同时使这个进程彻底消失。

阅读(1326) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~