日志标签 ‘进程’

动态的进程

2011年9月29日

进程是一个动态的实体,它是程序的一次执行过程,是操作系统中资源分配的基本单位。程序和进程的最大区别在于进程是动态运行着的,它存在于内存中;程序是静态的,它存在于磁盘上。

这里的程序指的是可执行文件(并不包含脚本文件),通常我们所说的源程序要通过预编译、编译、汇编和链接四个步骤转化为可执行程序。当我们运行可执行程序时,操作系统将可执行程序读入内存,程序摇身一变转化为一个进程。

1.进程的状态

进程是一个活体,因此也就随之诞生了进程的状态。在Linux内核中,几种经典的进程状态如下:

可运行状态:表示进程正在运行或者正在等待被运行,也就是操作系统原理中的运行态和就绪态。Linux使用TASK_RUNNING宏表示此状态。

可中断的等待状态:进程正在等待某个事件完成,即操作系统原理中等待态(或阻塞态、睡眠态)。处于该状态的进程属于“轻度睡眠”,它可以被信号或者定时器唤醒。Linux中使用TASK_INTERRUPTIBLE来表示此状态。

不可中断的等待状态:也是一种等待状态,只不过处于该状态的进程处于“深度睡眠”,它不能被信号或者定时器唤醒,只有当其等待的事件发生时才可以被唤醒。Linux中使用TASK_UNINTERRUPTIBLE来表示该状态。

僵死状态:进程已经终止,但是它的进程描述符仍然没有被操作系统收回,此时的进程只有身躯并无灵魂,需要父进程通过wait族函数为其收尸。Linux中使用EXIT_ZOMBIE表示该状态。

停止状态:进程因为收到SIGSTOP等信号停止运行。Linux中使用__TASK_TRACED表示该状态。

除了上述几种经典状态外,可以在sched.h中查看其他几种进程的状态。我们通过ps命令在shell中查看当前系统所有进程的状态信息。比如:

edsionte@edsionte-desktop:~/linux-3.0.4$ ps -eo pid,stat,command
  PID STAT COMMAND
    1 Ss   /sbin/init
    2 S    [kthreadd]
    3 S    [migration/0]
    4 S    [ksoftirqd/0]
    5 S    [watchdog/0]
    6 S    [migration/1]
    7 S    [ksoftirqd/1]
    8 S    [watchdog/1]

每行依次显示的是进程的pid、进程当前的状态和进程名。ps命令所显示的进程状态通过一组字母符号来表示,各种符号的含义如下:

       D    Uninterruptible sleep (usually IO)
       R    Running or runnable (on run queue)
       S    Interruptible sleep (waiting for an event to complete)
       T    Stopped, either by a job control signal or because it is being traced.
       W    paging (not valid since the 2.6.xx kernel)
       X    dead (should never be seen)
       Z    Defunct ("zombie") process, terminated but not reaped by its parent.

2.进程的内存映像

在Linux中可执行文件(此处先忽略脚本文件)的格式为ELF,即Executable and Linking Format。虽然可执行文件是二进制文件,但是它的内部还是划分着一些区域,这些区域称为可执行文件的段。这里的段和X86体系架构中的段是两个完全不同的概念。

通常可执行文件包含一个文本段(代码段),数据段。有时候可执行文件中还额外包含一个BSS段,这个段中存放着那些在源程序中没有被初始化的全局变量。

当运行可执行文件时,内核将可执行文件读入内存,我们将读入内存中的可执行文件称之为进程的内存映像。进程的内存映像虽然也根据存放内容的不同将进程的虚拟地址空间划分为一块一块的,但是这些区域在内存中并不称之为段,而是虚拟内存区域,内核使用vm_area_struct结构来表示这片内存区域。

可执行文件和进程的内存映像虽然都有代码段和数据段,但两者其实是不同的。首先可执行文件(也就是程序)是静态的,存放在磁盘上,而进程的内存映像只有在程序运行时才产生;其次,可执行文件没有堆栈段,而进程的内存映像是包含堆栈段的,因为堆(heap)用于动态分配内存,而栈(stack)需要保存局部变量、临时数据和传递到函数的参数等。从这些不同点也可以说明程序是动态的。

可执行文件的段在进程地址空间中的分布图可以参考如下:

需要注意的是,进程的地址空间并不只包含一个文本段或数据段,由于大多数源程序的目标文件在链接时都需要链接一些动态库,最终形成可执行文件,因此进程的地址空间中会有好几个文本段和数据段区域。而且在链接过程中,还需要加入链接器(ld),因此进程地空间精确的示意图如下:

 

在一个进程的内存映像中,文本段用来存放二进制的可执行代码,而数据段用来存放全局变量和静态变量,BSS段用来存放未初始化的全局变量,堆段用于为malloc函数分配空间,栈段用来存放局部变量和形参等临时数据。

参考:

1. Linux C编程实战
2. C专家编程

进程在Linux内核中的角色扮演

2011年9月2日

在Linux内核中,内核将进程、线程和内核线程一视同仁,即内核使用唯一的数据结构task_struct来分别表示他们;内核使用相同的调度算法对这三者进行调度;并且内核也使用同一个函数do_fork()来分别创建这三种执行线程(thread of execution)。执行线程通常是指任何正在执行的代码实例,比如一个内核线程,一个中断处理程序或一个进入内核的进程。

这样处理无疑是简洁方便的,并且内核在统一处理这三者之余并没有失去他们本身所具有的特性。本文将结合进程、线程和内核线程的特性浅谈进程在内核中的角色扮演问题。

1.进程描述符task_struct的多角色扮演

上述三种执行线程在内核中都使用统一的数据结构task_struct来表示。task_struct结构即所谓的进程描述符,它包含了与一个进程相关的所有信息。进程描述符中不仅包含了许多描述进程属性的字段,而且还有一系列指向其他数据结构的指针。下面将简单介绍进程描述符中几个比较特殊的字段,它们分别指向代表进程所拥有的资源的数据结构。

mm字段:指向mm_struct结构的指针,该类型用来描述进程整个的虚拟地址空间。

fs字段:指向fs_struct结构的指针,该类型用来描述进程所在文件系统的根目录和当前进程所在的目录信息。

files字段:指向files_struct结构的指针,该类型用来描述当前进程所打开文件的信息。

signal字段:指向signal_struct结构(信号描述符)的指针,该类型用来描述进程所能处理的信号。

对于普通进程来说,上述字段分别指向具体的数据结构以表示该进程所拥有的资源。

对应每个线程而言,内核通过轻量级进程与其进行关联。轻量级进程之所轻量,是因为它与其他进程共享上述所提及的进程资源。比如进程A创建了线程B,则B线程会在内核中对应一个轻量级进程。这个轻量级进程很自然的对应一个进程描述符,只不过B线程的进程描述符中的某些代表资源指针会和A进程中对应的字段指向同一个数据结构,这样就实现了多线程之间的资源共享。

由于内核线程只运行在内核态,并且只能由其他内核线程创建,所以内核线程并不需要和普通进程那样的独立地址空间。因此内核线程的进程描述符中的mm指针即为NULL。内核线程是否共享父内核线程的某些资源,则通过向内核线程创建函数kernel_thread()传递参数来决定。

通过上面的分析可以发现,内核中使用统一的进程描述符来表示进程、线程和内核线程,根据他们不同的特性,其进程描述符中某些代表资源的字段的指向会有所不同,以实现扮演不同角色。

2. do_fork()的多角色扮演

进程、线程以及内核线程都有对应的创建函数,不过这三者所对应的创建函数最终在内核都是由do_fork()进行创建的,具体的调用关系图如下:

从图中可以看出,内核中创建进程的核心函数即为看do_fork(),该函数的原型如下:

long do_fork(unsigned long clone_flags,
               unsigned long stack_start,
               struct pt_regs *regs,
               unsigned long stack_size,
               int __user *parent_tidptr,
               int __user *child_tidptr)

该函数的参数个数是固定的,每个参数的功能如下:

clone_flags:代表进程各种特性的标志。低字节指定子进程结束时发送给父进程的信号代码,一般为SIGCHLD信号,剩余三个字节是若干个标志或运算的结果。

stack_start:子进程用户态堆栈的指针,该参数会被赋值给子进程的esp寄存器。

regs:指向通用寄存器值的指针,当进程从用户态切换到内核态时通用寄存器中的值会被保存到内核态堆栈中。

stack_size:未被使用,默认值为0。

parent_tidptr:该子进程的父进程用户态变量的地址,仅当CLONE_PARENT_SETTID被设置时有效。

child_tidptr:该子进程用户态变量的地址,仅当CLONE_CHILD_SETTID被设置时有效。

既然进程、线程和内核线程在内核中都是通过do_fork()完成创建的,那么do_fork()是如何体现其功能的多样性?其实,clone_flags参数在这里起到了关键作用,通过选取不同的标志,从而保证了do_fork()函数实现多角色——创建进程、线程和内核线程——功能的实现。clone_flags参数可取的标志很多,下面只介绍几个与本文相关的标志。

CLONE_VIM:子进程共享父进程内存描述符和所有的页表。

CLONE_FS:子进程共享父进程所在文件系统的根目录和当前工作目录。

CLONE_FILES:子进程共享父进程打开的文件。

CLONE_SIGHAND:子进程共享父进程的信号处理程序、阻塞信号和挂起的信号。使用该标志必须同时设置CLONE_VM标志。

如果创建子进程时设置了上述标志,那么子进程会共享这些标志所代表的父进程资源。

2.1 进程的创建

在用户态程序中,可以通过fork()、vfork()和clone()三个接口函数创建进程,这三个函数在库中分别对应同名的系统调用。系统调用函数通过128号软中断进入内核后,会调用相应的系统调用服务例程。这三个函数对应的服务历程分别是sys_fork()、sys_vfork()和sys_clone()。

 int sys_fork(struct pt_regs *regs)
 {
         return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
 }

 int sys_vfork(struct pt_regs *regs)
 {
         return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,
                        NULL, NULL);
 }

 long
 sys_clone(unsigned long clone_flags, unsigned long newsp,
           void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
 {
         if (!newsp)
                 newsp = regs->sp;
         return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
 }

通过上述系统调用服务例程的源码可以发现,三个服务历程内部都调用了do_fork(),只不过差别在于第一个参数所传的值不同。这也正好导致由这三个进程创建函数所创建的进程有不同的特性。下面对每种进程作以简单说明。

fork():由于do_fork()中clone_flags参数除了子进程结束时返回给父进程的SIGCHLD信号外并无其他特性标志,因此由fork()创建的进程不会共享父进程的任何资源。子进程会完全复制父进程的资源,也就是说父子进程相对独立。不过由于写时复制技术(Copy On Write,COW)的引入,子进程可以只读父进程的物理页,只有当两者之一去写某个物理页时,内核此时才会将这个页的内容拷贝到一个新的物理页,并把这个新的物理页分配给正在写的进程。

vfork():do_fork()中的clone_flags使用了CLONE_VFORK和CLONE_VM两个标志。CLONE_VFORK标志使得子进程先于父进程执行,父进程会阻塞到子进程结束或执行新的程序。CLONE_VM标志使得子进程共享父进程的内存地址空间(父进程的页表项除外)。在COW技术引入之前,vfork()适用子进程形成后立马执行execv()的情形。因此,vfork()现如今已经没有特别的使用之处,因为写实复制技术完全可以取代它创建进程时所带来的高效性。

clone():clone通常用于创建轻量级进程。通过传递不同的标志可以对父子进程之间数据的共享和复制作精确的控制,一般flags的取值为CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND。由上述标志可以看到,轻量级进程通常共享父进程的内存地址空间、父进程所在文件系统的根目录以及工作目录信息、父进程当前打开的文件以及父进程所拥有的信号处理函数。

2.2 线程的创建

每个线程在内核中对应一个轻量级进程,两者的关联是通过线程库完成的。因此通过pthread_create()创建的线程最终在内核中是通过clone()完成创建的,而clone()最终调用do_fork()。

2.3 内核线程的创建

一个新内核线程的创建是通过在现有的内核线程中使用kernel_thread()而创建的,其本质也是向do_fork()提供特定的flags标志而创建的。

 int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
 {
        /*some register operations*/
         return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
 }

从上面的组合的flag可以看出,新的内核线程至少会共享父内核线程的内存地址空间。这样做其实是为了避免赋值调用线程的页表,因为内核线程无论如何都不会访问用户地址空间。CLONE_UNTRACED标志保证内核线程不会被任何进程所跟踪,

3. 进程的调度

由于进程、线程和内核线程使用统一数据结构来表示,因此内核对这三者并不作区分,也不会为其中某一个设立单独的调度算法。内核将这三者一视同仁,进行统一的调度。

参考资料:

1. 深入理解Linux内核

2. Linux内核设计与实现

C/S模型的健壮性-为僵死进程收尸

2011年4月21日

并发服务器可以同时处理多个客户端的请求,服务器对客户端网络请求的处理是通过fork子服务进程来完成的。当有多个客户请求时,主服务器进程就会产生多个子服务器进程。当这些子服务器进程处理完客户请求时,就终止运行了。由于主服务器进程(也就是这些子服务器进程的父亲)仍然在运行,因此这些终止的子服务器进程在主服务器退出之前就成为了僵死进程。通常服务器进程都长时间处于运行状态,所以这些僵死进程就会一直存在于系统内。如果不及时清除这些僵死进程,他们将占用内核的空间,最终可能会耗尽系统资源。本文将描述如何清理这些僵死进程。

1. 僵死进程的产生

下面将通过一个简单的C/S模型回射程序来演示僵死进程的产生。我们首先将服务器置于后台运行,然后再查看当前终端(pts/3)下的进程状态:

edsionte@edsionte-desktop:~/echoCS$ ps -o pid,ppid,args,stat,wchan
  PID  PPID COMMAND                     STAT WCHAN
 5071  2305 bash                        Ss   wait
 6146  5071 ./server                    S    inet_csk_wait_for_connect
 6149  5071 ps -o pid,ppid,args,stat,wc R+   -

由上可以看出,服务器进程处于等待连接的状态,此刻它正在监听客户请求。接着运行客户端程序,连接成功后输入任意的字符串,可以看到服务器和客户端通信正常。

edsionte@edsionte-desktop:~/echoCS$ ./client 127.0.0.1
received a connection from:127.0.0.1
ps
ps

我们在另一个终端下,查看位于终端pts/3下客户端和服务器进程的状态。

edsionte@edsionte-desktop:~/echoCS$ ps -t pts/3 -o pid,ppid,args,stat,wchan
  PID  PPID COMMAND                     STAT WCHAN
 5071  2305 bash                        Ss   wait
 6146  5071 ./server                    S    inet_csk_wait_for_connect
 6150  5071 ./client 127.0.0.1          S+   n_tty_read
 6151  6146 ./server                    S    sk_wait_data

由于客户进程发来连接请求,因此服务器进程fork了一个子服务器进程,从pid和ppid可以看到两个服务器进程之间的父子关系。父子进程此刻都处于等待状态,不过他们等待的目标不同:父进程监听客户请求,子进程则阻塞于read函数等待套接字数据的来临。

接下来通过发信号SIGINT(通过键盘上的ctrl+c可发出此信号)使客户进程终止。

edsionte@edsionte-desktop:~/echoCS$ ./client 127.0.0.1
received a connection from:127.0.0.1
ps
ps
^C

我们在另一个终端查看pty/3终端当前进程的状态:

edsionte@edsionte-desktop:~/echoCS$ ps -t pts/3 -o pid,ppid,args,stat,wchan
  PID  PPID COMMAND                     STAT WCHAN
 5071  2305 bash                        Ss+  n_tty_read
 6146  5071 ./server                    S    inet_csk_wait_for_connect
 6151  6146 [server]           Z    exit

可以看到,子服务器进程现在处于僵死状态。目前我们只有运行了一次客户端程序,如果一个服务器的网络请求繁忙,则会产生很多僵死进程。

2.处理僵死进程

当子进程终止时会给父进程发送SIGCHLD信号,因此我们可以利用信号处理函数捕获这个信号并对僵死进程进行处理。我们知道在父进程中调用wait函数可以防止先于父进程终止的子进程编程僵死进程,因此我们的信号捕获函数如下所示:

void sig_zchild(int signo)
{
	pid_t pid;
	int stat;

	pid = wait(&stat);
        printf("child %d terminated\n", pid);
	return;
}

并且我们需要适当的修改服务器程序,在accept函数调用之前调用signal函数:

	if(listen(sockfd, BACKLOG) == -1) {

		printf("listen error!\n");
		exit(1);
	}

	if (signal(SIGCHLD, sig_zchild) == SIG_ERR) {
		printf("signal error!\n");
		exit(1);
	}

	while (1) {

		sin_size = sizeof(struct sockaddr_in);
		if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr,
&sin_size)) == -1) {

			printf("accept error!\n");
			continue;
		}
		…… ……
	}//while

我们下面进行测试,我们首先在终端后台运行服务器程序,然后同时在其他多个终端分别运行客户端程序。当前终端的进程状态如下:

edsionte@edsionte-desktop:~/echoCS$ ps
  PID TTY          TIME CMD
 6699 pts/0    00:00:00 bash
 6926 pts/0    00:00:00 server
 6947 pts/0    00:00:00 server
 6966 pts/0    00:00:00 server
 6985 pts/0    00:00:00 server
 6986 pts/0    00:00:00 ps

然后我们分别在运行客户程序的终端杀死客户端进程。再次查看当前终端下的进程状态,可以发现并没有僵死的子服务进程。因为每个子进程在终止时发送SIGCHLD信号都会被父进程中的信号处理函数捕获。

3.僵死进程处理的加强版

上述的客户端进程每次向服务器端只发出一个连接请求,当客户端终止时子服务器进程将终止;如果一个客户端向服务器发出多个连接请求,当该客户端终止时多个子服务器进程将同时终止,也就是说父服务器进程将面临同时处理多个SIGCHLD信号的情况。

按照上述的特殊客户端,我们将回射C/S模型中的客户端适当修改,使运行一次客户端就连接服务器5次,这样就会产生相应的5个子服务器进程。

	int i;
	for (i = 0; i < 5; i++) {
		if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
              		printf("socket error!\n");
         		exit(1);
         	}
	
		serv_addr.sin_family = AF_INET;
          	serv_addr.sin_port = htons(SERV_PORT);
         	serv_addr.sin_addr = *((struct in_addr *)host->h_addr);

            	if(connect(sockfd, (struct sockaddr *)&serv_addr,
					sizeof(struct sockaddr)) == -1) {
			printf("connect error!\n");
	          	exit(1);
            	}
	}

	str_cli(stdin, sockfd);

运行服务器程序和刚刚修改过的客户端程序,我们作如下测试:

edsionte@edsionte-desktop:~/echoCS$ ps -t pts/0 -o pid,ppid,args,stat
  PID  PPID COMMAND                     STAT
 2651  2649 bash                        Ss
 2861  2651 ./server                    S
 2865  2651 ./mclient 127.0.0.1         S+
 2866  2861 ./server                    S
 2867  2861 ./server                    S
 2868  2861 ./server                    S
 2869  2861 ./server                    S
 2870  2861 ./server                    S
edsionte@edsionte-desktop:~/echoCS$ kill 2865
edsionte@edsionte-desktop:~/echoCS$ ps -t pts/0 -o pid,ppid,args,stat
  PID  PPID COMMAND                     STAT
 2651  2649 bash                        Ss+
 2861  2651 ./server                    S
 2867  2861 [server]           Z
 2868  2861 [server]           Z
 2869  2861 [server]           Z
 2870  2861 [server]           Z

当杀死客户端进程后,只有一个子服务器进程被SIGCHLD信号处理函数捕获处理,其他四个子服务器进程仍然处于僵死状态。出现这种现象的原因是,主服务器进程中的信号处理函数使用了wait函数,该函数会一直阻塞到出现第一个终止的子进程。也就是说,该函数只等待第一个终止的子进程并返回。

本客户端程序终止后,将同时产生5个SIGCHLD信号,而wait函数只处理其中一个,解决这个问题的办法是使用waitpid函数。该函数将周期性的检查是否有子进程终止,也就是说可以等待所有的终止子进程。修改后的服务器信号处理函数如下:

void sig_zchild(int signo)
{
	pid_t pid;
	int stat;

	while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
		printf("child %d terminated\n", pid);

	return;
}

按此修改后SIGCHLD信号处理函数后,就可以避免留下僵死进程。

C/S模型中的并发服务器

2011年4月20日

网络通信程序中最经典的模型就是客户端/服务器(C/S)模型。该模型中的服务器程序通常处于长时间运行的状态,当客户端程序主动对服务器程序发出网络请求时,服务器程序才做出具体的回应。也就是说,服务器大多数处于等待状态,只有收到客户端的网络请求才被唤醒,当处理完客户端的请求时候,又将处于等待状态。

上述同时只能处理一个客户端请求的服务器被称为迭代服务器(iterative server),这样的C/S模型只能应对那些简单的应用。大多数情况下我们希望服务器可以同时服务多个客户端程序,即服务器程序既能处理客户端发送来的请求同时又能接收其他客户端发送的请求。这样的服务器称为并发服务器(concurrent server)。

Linux中实现并发服务器的方法很简单:每当客户端对服务器有网络请求时,服务器程序就fork一个子服务器进程来处理这个客户的请求,而主服务器程序仍处于等待其他客户端网络请求的状态。基于这个原理,并发服务器的基本模型如下:

	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		my_error("socket", errno, __LINE__);
	}

	if (bind(sockfd, (struct sockaddr *)&my_addr,
				sizeof(struct sockaddr_in)) == -1) {
		my_error("bind", errno, __LINE__);
	}

	if (listen(sockfd, BACKLOG) == -1) {
		my_error("listen", errno, __LINE__);
	}

	while (1) {
		sin_size = sizeof(struct sockaddr_in);
		if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1) {
			my_error("accept", errno, __LINE__);
			continue;
		}

		if ((pid = fork()) == 0) {
			close(sockfd);
			process_client_request(client_fd);
			close(client_fd);
			exit(0);
		} else if (pid > 0)
			close(client_fd);
		else
			my_error("fork", errno, __LINE__);
	}

每当accept()接收到一个TCP连接时,主服务器进程就fork一个子服务器进程。子服务器进程调用相应的函数,通过client_fd(连接套接字)对客户端发来的网络请求进程处理;由于客户端的请求已被子服务进程处理,那么主服务器进程就什么也不做,通过sockfd(监听套接字)继续循环等待新的网络请求。

这里我们需要特别强调的是,在父子进程中需要关闭相应的套接字描述符。从上述代码中可以看到,子服务进程在处理客户端的网络请求之前关闭了监听套接字,主服务进程则关闭了连接套接字,子服务进程在处理完客户端请求后关闭了连接套接字。最后一种关闭套接字的情形比较合乎常理,这里我们重点关注前两种关闭套接字的情况。

我们的问题是:子服务进程关闭了监听套接字,服务器是否还能监听其他连接请求;父服务进程关闭了连接套接字,服务器是否还能够处理客户端请求。

如果理解了文件的的引用计数,这个问题就会迎刃而解。每个文件都有一个引用计数,该引用计数表示当前系统内的所有进程打开该文件描述符的个数。套接字是一种特殊的文件,当然也有引用计数。

在并发服务器这个例子当中,在accept函数之前,sockfd的引用计数为1;在fork函数执行之前,sockfd和client_fd的引用计数分别为1;当fork执行后,由于子进程复制了父进程的资源,所以子进程也拥有这两个套接字描述符,则此时sockfd和client_fd的引用计数都为2。只有当子进程处理完客户请求时,client_fd的引用计数才由于close函数而变为0。由于父服务器进程的主要任务是监听客户请求,则它关闭了连接套接字client_fd;而子进程的主要任务是处理客户请求,它不必监听其他客户请求,因此子进程关闭了sockfd。由上可知,父子进程关闭相应的套接字并不会影响其所负责的通信功能。

我们可以通过下面的两幅示意图更进一步了解fork函数执行前后的客户端和服务器之间的状态。

父子进程关闭相应套接字后客户端和服务器端的状态如下:

这就是监听套接字和连接套接字两者的最终状态,子进程处理客户请求,父进程监听其他客户请求。这样的并发服务器看似完美,但是会产生许多僵死进程,这是为什么?下文将会分析。

Linux2.6进程调度分析(3)-与调度有关的函数分析

2011年4月8日

前面两篇文章从原理角度分析了进程的调度,本文将从具体的源码出发,分析与进程进程调度密切相关的几个函数。

1.时间片的分配:task_timeslice()

正如我们所知的那样,进程的时间片与进程的静态优先级有直接的关系。从代码中可以看到,根据进程静态优先级static_prio与NICE_TO_PRIO(0)的大小关系,进程时间片的分配可以分为两条路线。以下代码如无特别说明均位于linux/kernel/sched.c下。

static unsigned int task_timeslice(task_t *p)
{
	if (p->static_prio < NICE_TO_PRIO(0)) 		return SCALE_PRIO(DEF_TIMESLICE*4, p->static_prio);
	else
		return SCALE_PRIO(DEF_TIMESLICE, p->static_prio);
}

NICE_TO_PRIO以及PRIO_TO_NICE宏的作用将进行nice值和进程静态优先级之间的转换。nice也用来表示进程的静态优先级,只不过它与静态优先级的大小范围不同,因此可以将nice看作是static_prio的缩影。

#define MAX_USER_RT_PRIO        100
#define MAX_RT_PRIO             MAX_USER_RT_PRIO
#define NICE_TO_PRIO(nice)	(MAX_RT_PRIO + (nice) + 20)
#define PRIO_TO_NICE(prio)	((prio) - MAX_RT_PRIO - 20)

目前我们已经知道普通进程的静态优先级大小范围是100到139,因此从上面的一些列宏可以得知,nice的取值范围为-20到19。

因此,NICE_TO_PRIO(0)取值为120,也就是说进程时间片分配的两条路线是根据静态优先级120进行划分的。从SCALE_PRIO宏的定义我们可以看到,该宏的作用是取两个数值(具体应该是时间片)的最大者。

#define MAX_PRIO                (MAX_RT_PRIO + 40)
#define USER_PRIO(p)		((p)-MAX_RT_PRIO)
#define MAX_USER_PRIO		(USER_PRIO(MAX_PRIO))
#define DEF_TIMESLICE		(100 * HZ / 1000)
#define MIN_TIMESLICE           max(5 * HZ / 1000, 1)
# define HZ             1000  //位于linux/include/asm-i386/param.h
#define SCALE_PRIO(x, prio) \
max(x * (MAX_PRIO - prio) / (MAX_USER_PRIO/2), MIN_TIMESLICE)

从上面的宏定义可知,(MAX_USER_PRIO/2)为20。当进程静态优先级小于120时,x的值为DEF_TIMESLICE*4,具体即为400ms;否则x为100ms。因此对于SCALE_PRIO宏可以用下面的公式来表达:

静态优先级<120,基本时间片=max((140-静态优先级)*20, MIN_TIMESLICE)

静态优先级>=120,基本时间片=max((140-静态优先级)*5, MIN_TIMESLICE)

其中MIN_TIMESLICE为系统所规定的最小时间片大小。

2.对可运行队列的操作

在优先级数组结构prio_array中,数组queue用来表示系统中每种优先级进程所形成的可运行队列,而且过期进程和活动进程分别对应这样一个数组。

如果进程仍然处于活动进程队列中,即说明该进程的时间片未用完。当该进程的时间片用完时就需要离开活动进程队列并进入过期进程队列。可运行进程进入进程队列是通过enqueue_task函数完成的,而离开进程队列是通过dequeue_task函数完成的。

每个进程的task_struct结构中都有list_head类型的run_list字段,将进程从可运行队列中删除起始就是对双联表的操作,同时我们需要更新优先级数组结构中活动进程的数目nr_active。如果当前进程优先级所对应的可运行队列已空,那么还要清除优先级位图中该进程优先级所对应的那个位。

如果要进程某个可运行队列,所做的工作基本上跟删除相反。不过该函数首先通过sched_info_queued函数更新该进程最后进入可运行队列的时间戳,并且在最后更新该进程描述符中的array字段,该字段指向当前进程所在的优先级数组。

 static void dequeue_task(struct task_struct *p, prio_array_t *array)
  {
          array->nr_active--;
          list_del(&p->run_list);
          if (list_empty(array->queue + p->prio))
                  __clear_bit(p->prio, array->bitmap);
  }

  static void enqueue_task(struct task_struct *p, prio_array_t *array)
  {
          sched_info_queued(p);
          list_add_tail(&p->run_list, array->queue + p->prio);
          __set_bit(p->prio, array->bitmap);
          array->nr_active++;
         p->array = array;
  }

3.schedule_tick()

schedule_tick函数用来更新进程的时间片,它被调用时本地中断被禁止,该函数的具体操作如下。

1.首先通过相应的函数和宏获得当前处理器的编号、当前可运行队列和当前进程描述符就,再通过sched_clock函数获得最近一次定时器中断的时间戳。如果array没有指向本地活动进程队列,则设置TIF_NEED_RESCHED标志,以便在稍候强制进程重新调度。

void scheduler_tick(void)
{
	int cpu = smp_processor_id();
	runqueue_t *rq = this_rq();
	task_t *p = current;

	rq->timestamp_last_tick = sched_clock();

	if (p == rq->idle) {
		if (wake_priority_sleeper(rq))
			goto out;
		rebalance_tick(cpu, rq, SCHED_IDLE);
		return;
	}
	if (p->array != rq->active) {
		set_tsk_need_resched(p);
		goto out;
	}

2.由于实时进程和普通进程的调度方法不同,因此这两种进程对时间片的更新方式也有所不同,下面仅说明普通进程更新时间片的方式。如果当前进程是普通进程,则递减当前进程的时间片。
3.如果当前进程时间片用完,首先从当前活动进程集合中删除该进程,然后通过set_tsk_need_resched函数设置TIF_NEED_RESCHED标志。

接着通过effective_prio函数更新当前进程的动态优先级,在进程调度的基本原理中我们已经知道进程的动态优先级是以进程的静态优先级(static_prio)为基数,在通过bonus适当的对其惩罚或奖励。

static int effective_prio(task_t *p)
{
	int bonus, prio;

	if (rt_task(p))
		return p->prio;

	bonus = CURRENT_BONUS(p) - MAX_BONUS / 2;

	prio = p->static_prio - bonus;
	if (prio < MAX_RT_PRIO) 		prio = MAX_RT_PRIO; 	if (prio > MAX_PRIO-1)
		prio = MAX_PRIO-1;
	return prio;
}

通过effective_prio函数,我们可以总结出进程动态优先级的计算规则:

动态优先级=max(100 , min(静态优先级 – bonus + 5) , 139)

再通过task_timeslice函数对当前进程重新分配时间片,因为我现在所处的分析环境是进程的时间片已经用完。然后将first_time_slice的值设置为0,说明当前进程的时间片可以用完。

上述过程的代码描述如下:

	if (rt_task(p)) {
		/*
		 *更新实时进程的时间片
		 */
	}
	if (!--p->time_slice) {
		dequeue_task(p, rq->active);
		set_tsk_need_resched(p);
		p->prio = effective_prio(p);
		p->time_slice = task_timeslice(p);
		p->first_time_slice = 0;

4.运行队列结构中的expired_timestamp字段用来描述过期进程队列中最早进程被插入队列的时间,如果本地运行队列中该字段等于0,则说明当前过期进程集合为空。因此将当前的时钟节拍赋值给该字段。

由于当前进程的时间片已经用完,因此接下来应该判定是将当前进程插入活动进程集合还是过期进程集合。可能此时你会有疑惑:既然当前进程的时间片已经用完,就应该直接插入过期进程队列,为何还要进行判断插入那个进程集合?

正如基本原理中所说的,调度程序总偏向交互进程以提高系统的响应能力。因此当交互型进程使用完时间片后,调度程序总是重新填充时间片并把它留在活动进程集合中。但调度程序并不永远都偏向交互型程序,如果最早进入过期进程集合的进程已经等待了很长时间,或过期进程的静态优先级比交互进程的静态优先级高,此时调度程序就会将时间片用完的交互进程转移到过期进程集合中。EXPIRED_STARVING宏完成的工作就是判断上述两种情况,至少其一否和,该宏就产生值1。

如果说当前进程已经移入到过期进程集合中,还需根据当前进程的优先级更新运行队列结构中的best_expired_prio字段,该字段用于记录过期进程集合中最高的静态优先级。

如果当前进程是交互进程,而且不满足EXPIRED_STARVING宏,则直接将该交互进程继续插入活动进程集合中。

		if (!rq->expired_timestamp)
			rq->expired_timestamp = jiffies;
		if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) {
			enqueue_task(p, rq->expired);
			if (p->static_prio < rq->best_expired_prio)
				rq->best_expired_prio = p->static_prio;
		} else
			enqueue_task(p, rq->active);

5.如果当前进程并未使用完时间片,则检查当前进程的剩余时间片是否太长。如果当前进程时间片过长的话,就将该进程的时间片分成若干个更小的时间段,这样可以防止拥有较长时间片的进程长时间霸占CPU。并且调度程序会将这样的进程放入与该进程优先级所对应的活动进程队列的末尾,稍候再次对其集成调度。

	} else {
		if (TASK_INTERACTIVE(p) && !((task_timeslice(p) -
			p->time_slice) % TIMESLICE_GRANULARITY(p)) &&
			(p->time_slice >= TIMESLICE_GRANULARITY(p)) &&
			(p->array == rq->active)) {

			requeue_task(p, rq->active);
			set_tsk_need_resched(p);
		}
	}

至此,该函数分析完毕。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key