日志标签 ‘并发’

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信号处理函数后,就可以避免留下僵死进程。

生产者-消费者模型(使用内核线程实现)

2010年10月25日

在IPC系列文章当中,利用信号量集实现的是用户态下生产者-消费者模型的模型。本文以内核模块的方式,通过创建内核线程来为大家演示内核态下的生产者消费者模型。本模型属于np-nc-nb,即多个生产者多个消费着多个缓冲区。

在加载函数中,完成一些初始化的工作,并分别创建了5个生产者线程和消费者线程。kernel_thread函数的第一个参数是所所创建线程要做的动作;通过第二个参数传递i变量。

static int __init np_nc_init(void)
{
	int i;

	printk("np_nc module is working..\n");
	in=out=0;
	cflag=0;
	init_MUTEX(&mutex);
	sema_init(&s1,BUF_NUM);
	sema_init(&s2,0);

	for(i=0;i< N;i++)
	{
		index[i]=i+1;
		kernel_thread(productor_thread,&(index[i]),CLONE_KERNEL);
		kernel_thread(consumer_thread,&(index[i]),CLONE_KERNEL);
	}

	return 0;
}

在生产者函数中,PNUM是每个生产者要生产货物的数量。语句buf[in]=i*100+(PNUM-p_num+1)即是产生货物的过程。

int productor_thread(void *p)
{
	int i=*(int *)p;
	int p_num=PNUM;

	while(p_num)
	{
		if((s1.count)<=0)
		{
			printk("[producer %d,%d]:I will be waiting for producting..\n",i,s1.count);
		}
		down(&s1);
	        down(&mutex);
          	buf[in]=i*100+(PNUM-p_num+1);
          	printk("[producer %d,%d]:I producted a goods \"%d\" to buf[%d]..\n",i,s1.count,buf[in],in);
            	in=(in+1)%BUF_NUM;
         	up(&mutex);
          	up(&s2);
		p_num--;
	}
	return 0;
}

在消费者函数中,通过一个全局变量CNUM来控制消费者总共的消费次数。

int consumer_thread(void *p)
{
	int i=*(int *)p;
	int goods;

	while(cflag!=CNUM)
	{
		if((s2.count)<=0)
		{
			printk("[consumer %d,%d]:I will be waiting for goods..\n",i,s2.count);
		}
	        down(&s2);
            	down(&mutex);
          	goods=buf[out];
           	printk("[consumer %d,%d]:I consumed a goods \"%d\" from buf[%d]..\n",i,s2.count,goods,(out%BUF_NUM));
           	out=(out+1)%BUF_NUM;
            	up(&mutex);
            	up(&s1);
		cflag++;
	}
	return 0;
}

内核同步方法-原子操作

2010年10月20日

Latest Update:2010/10/23

原子操作

原子操作用于执行轻量级、仅执行一次的操作,比如修改计数器,某些条件下的增加值或设置位等。原子操作指的是指令以原子的方式执行。之所以用原子来修饰这种操作方式是因为它们均不可再分。也就是说,原子操作要么一次性执行完毕,要么就不执行,这些操作的执行过程是不可被打断的。原子操作的具体实现取决于体系架构,以下代码如无特别声明,均来自linux/arch/x86/include/asm/atomic.h中

内核中提供了两组原子操作的接口:原子整形操作和原子位操作。前者是一组对整形数据的操作,后者则是针对单独的位操作。通过上面的叙述我们可以知道,这写操作接口完成的动作都是原子的。

原子整形操作

0.数据结构

在linux/include/linux/types.h中有如下定义:

 190typedef struct {
 191        int counter;
 192} atomic_t;

原子类型内部只有一个整型的成员counter。

1.初始化/设置原子变量的值

  15#define ATOMIC_INIT(i)  { (i) }

  35static inline void atomic_set(atomic_t *v, int i)
  36{
  37        v->counter = i;
  38}

初始化宏的源码很明显的说明了如何初始化一个原子变量。我们在定义一个原子变量时可以这样的使用它:

  atomic_t v=ATOMIC_INIT(0);

atomic_set函数可以原子的设置一个变量的值。

2.获取原子变量的值

  23static inline int atomic_read(const atomic_t *v)
  24{
  25        return (*(volatile int *)&(v)->counter);
  26}

返回原子型变量v中的counter值。关键字volatile保证&(v->counter)的值固定不变,以确保这个函数每次读入的都是原始地址中的值。

3.原子变量的加与减

   47static inline void atomic_add(int i, atomic_t *v)
  48{
  49        asm volatile(LOCK_PREFIX "addl %1,%0"
  50                     : "+m" (v->counter)
  51                     : "ir" (i));
  52}

  61static inline void atomic_sub(int i, atomic_t *v)
  62{
  63        asm volatile(LOCK_PREFIX "subl %1,%0"
  64                     : "+m" (v->counter)
  65                     : "ir" (i));
  66}

加减操作中使用了内联汇编语句。

linux中的汇编语句都采用AT&T指令格式。带有C表达式的内联汇编格式为:__asm__ __volatile__(“InstructionList” :Output :Input :Clobber/Modify);其中asm(或__asm__)用来声明一个内联汇编语句;关键字volatile是可选的,选择此关键字意味着向编译器声明“不要动我所写的指令,我需要原封不动的保留每一条指令”,否则,编译器可能会对指令进行优化。InstructionList即我们所加的汇编语句。指令后面的3个部分(Output, Input,Clobber/Modify)是可选的,分别指输出操作数,输入操作数,修改位。

下面我们针对上述加减操作对上述内联汇编语句做简单的解释。

在加函数中,对变量加操作的原子型表现在使用单条的汇编语句完成。指令中的%1,%0分别代表输入操作数和输出操作数,它们用来标示变量。内联汇编语句中的操作数从0开始进行编号,并且优先为输出操作数编号,然后再依次为输入操作数编号。在指令后的输出部分,“+m”中的+表示输出变量是可读可写的,m表示输出操作数存储于内存单元中,而不是在寄存器中。在输入部分,“ir”中的i表示输入操作数是一个直接操作数,r表示存储与寄存器中。输入部分和输出部分中的括号内的C语言格式的变量分别对应汇编语句中的输入操作数和输出操作数。

汇编语句addl %1,%0就是将i加至v->counter上,这个加的过程是原子的。

了解了加操作,那么减操作也就不难理解了,这里不再赘述。

4.原子变量的自加自减

  93static inline void atomic_inc(atomic_t *v)
  94{
  95        asm volatile(LOCK_PREFIX "incl %0"
  96                     : "+m" (v->counter));
  97}

 105static inline void atomic_dec(atomic_t *v)
 106{
 107        asm volatile(LOCK_PREFIX "decl %0"
 108                     : "+m" (v->counter));
 109}

从内联的汇编语句中可以看到,自加自减均采用单条汇编指令,直接在输出数%0上加1或减1。这里的输出数%0的值即是v->counter变量的值。

5.操作并测试

atomic_sub_and_test函数实现的功能是原子的从v减去i,如果结果等于0,则返回1;否则返回0。

 77static inline int atomic_sub_and_test(int i, atomic_t *v)
  78{
  79        unsigned char c;
  80
  81        asm volatile(LOCK_PREFIX "subl %2,%0; sete %1"
  82                     : "+m" (v->counter), "=qm" (c)
  83                     : "ir" (i) : "memory");
  84        return c;
  85}

通过上述源码可以看到,第一条指令实现减操作;接着根据ZF的值设置c变量的值。sete命令的作用是,如果ZF=1,也就是说减的结果为0,将设置c变量为1;否则c的值为0。执行完毕后返回相应的c值。

 119static inline int atomic_dec_and_test(atomic_t *v)
 120{
 121        unsigned char c;
 122
 123        asm volatile(LOCK_PREFIX "decl %0; sete %1"
 124                     : "+m" (v->counter), "=qm" (c)
 125                     : : "memory");
 126        return c != 0;
 127}

 137static inline int atomic_inc_and_test(atomic_t *v)
 138{
 139        unsigned char c;
 140
 141        asm volatile(LOCK_PREFIX "incl %0; sete %1"
 142                     : "+m" (v->counter), "=qm" (c)
 143                     : : "memory");
 144        return c != 0;
 145}

上数两个函数的作用是自减1或自加1后,再测试v值是否为0,如果是0,则返回1;否则返回0。

也许你会认为上述的加/减并测试并不是原子的。我个人的认为是:第一条加/减操作的确是原子的,当此条语句执行完毕后,会产生相应的ZF值。如果此时在执行sete之前产生了中断,那么肯定会保存当前寄存器等值。因此,即便中间产生了中断,返回的也是中断前加/减后的结果。

6.操作并返回

 173static inline int atomic_add_return(int i, atomic_t *v)
 174{
 175        int __i;
 176#ifdef CONFIG_M386
 177        unsigned long flags;
 178        if (unlikely(boot_cpu_data.x86 <= 3))
 179                goto no_xadd;
 180#endif
 181        /* Modern 486+ processor */
 182        __i = i;
 183        asm volatile(LOCK_PREFIX "xaddl %0, %1"
 184                     : "+r" (i), "+m" (v->counter)
 185                     : : "memory");
 186        return i + __i;
 187
 188#ifdef CONFIG_M386
 189no_xadd: /* Legacy 386 processor */
 190        raw_local_irq_save(flags);
 191        __i = atomic_read(v);
 192        atomic_set(v, i + __i);
 193        raw_local_irq_restore(flags);
 194        return i + __i;
 195#endif
 196}

首先if判断语句判断当前的处理器是否为386或386以下,如果是则直接执行no_xadd处开始的代码;否则紧接着判断语句执行。此处的unlikely表示”经常不“。因为现在的处理器大都高于386,所以编译器一般都按照高于386来处理,以达到优化源代码的目的。搞清楚这段代码的逻辑关系,那么接下来的工作就不困难了。

如果CPU高于386,则它将执行xaddl命令,该命令的作用是先交换源和目的操作数,再相加。这里%0,%1分别代表i和v->counter。首先将i值用__i保存;操作数交换后,%0,%1的值依次为v->counter和i;相加并保存到%1;最后%0,%1的结果就是v->counter,i+v->counter,返回的结果i+__i的结果就是i+v->counter。这里涉及到i和v->counter的值时均指初值。

理解了上述过程,再看其他的操作返回函数:

 205static inline int atomic_sub_return(int i, atomic_t *v)
 206{
 207        return atomic_add_return(-i, v);
 208}

可以看到,atomic_sub_return函数是对atomic_add_return函数的封装。而下面两个函数则又是对上述两个函数的封装。

 210#define atomic_inc_return(v)  (atomic_add_return(1, v))
 211#define atomic_dec_return(v)  (atomic_sub_return(1, v))

上述就是对整形原子操作的分析。

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