linux--多进程


例子:

删除进程
 typedef void (*sighandler_t)(int);

u_char                          cmd[1024];
sighandler_t                    old_handler;
    ngx_sprintf(cmd, "pid=`ps -ef|grep \"makets\"|grep -v grep|awk '{print $2}'`;"
"for i in $pid;do child=`ps -ef|grep $i|grep -v grep|awk '{print $2}'`;"
"kill -9 $i;kill -9 $child;done &");
old_handler = signal(SIGCHLD, SIG_DFL);
if ((fp = popen((char *)cmd, "r") ) == NULL) { 
ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, 0, "open cmd error");
    signal(SIGCHLD, old_handler); 
}
while(fgets(recvbuf, sizeof(recvbuf), fp));
ret = (ngx_int_t) pclose(fp);
signal(SIGCHLD, old_handler); 
printf("##############close ret is :%d\n", (int)ret);

1 引言

      对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值。fork函数是Unix系统最杰出的成就之一,它是七十年代UNIX早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程方法。与DOS和早期的Windows不同,Unix/Linux系统是真正实现多任务操作的系统,可以说,不使用多进程编程,就不能算是真正的Linux环境下编程。
多线程程序设计的概念早在六十年代就被提出,但直到八十年代中期,Unix系统中才引入多线程机制,如今,由于自身的许多优点,多线程编程已经得到了广泛的应用。
下面,我们将介绍在Linux下编写多进程和多线程程序的一些初步知识。

2 多进程编程

      什么是一个进程?进程这个概念是针对系统而不是针对用户的,对用户来说,他面对的概念是程序。当用户敲入命令执行一个程序的时候,对系统而言,它将启动一个进程。但和程序不同的是,在这个进程中,系统可能需要再启动一个或多个进程来完成独立的多个任务。多进程编程的主要内容包括进程控制和进程间通信,在了解这些之前,我们先要简单知道进程的结构。

2.1 Linux
下进程的结构
      Linux下一个进程在内存里有三部分的数据,就是"代码段"、"堆栈段"和"数据段"。其实学过汇编语言的人一定知道,一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分也是构成一个完整的执行序列的必要的部分。"代码段",顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。"堆栈段"存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。这其中有许多细节问题,这里限于篇幅就不多介绍了。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。

2.2 Linux
下的进程控制
      在传统的Unix环境下,有两个基本的操作用于创建和修改进程:函数fork( )用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝;函数族exec( )用来启动另外的进程以取代当前运行的进程。Linux的进程控制和传统的Unix进程控制基本一致,只在一些细节的地方有些区别,例如在Linux系统中调用vforkfork完全相同,而在有些版本的Unix系统中,vfork调用有不同的功能。由于这些差别几乎不影响我们大多数的编程,在这里我们不予考虑。

2.2.1 fork( )

      fork在英文中是"分叉"的意思。为什么取这个名字呢?因为一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就"分叉"了,所以这个名字取得很形象。下面就看看如何具体使用fork,这段程序演示了使用fork的基本框架:
#include<stdio.h>
#include<unistd.h>
void main(){
int i;
if ( fork() == 0 ) {
/*
子进程程序 */
for ( i = 1; i <1000; i ++ ) printf("This is child process\n");
}
else {
/*
父进程程序
*/
for ( i = 1; i <1000; i ++ ) printf("This is process process\n");
}
}
      程序运行后,你就能看到屏幕上交替出现子进程与父进程各打印出的一千条信息了。如果程序还在运行中,你用ps命令就能看到系统中有两个它在运行了。

那么调用这个fork函数时发生了什么呢?fork函数启动一个新的进程,前面我们说过,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。它们再要交互信息时,只有通过进程间通信来实现,这将是我们下面的内容。既然它们如此相象,系统如何来区分它们呢?这是由函数的返回值来决定的。对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。在操作系统中,我们用ps函数就可以看到不同的进程号,对父进程而言,它的进程号是由比它更低层的系统调用赋予的,而对于子进程而言,它的进程号即是fork函数对父进程的返回值。在程序设计中,父进程和子进程都要调用函数fork()

        上面的代码,我们就是利用fork函数对父子进程的不同返回值用if...else...语句来实现让父子进程完成不同的功能,正如我们上面举的例子一样。我们看到,上面例子执行时两条信息是交互无规则的打印出来的,这是父子进程独立执行的结果,虽然我们的代码似乎和串行的代码没有什么区别。
     读者也许会问,如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork的系统开销不是很大吗?其实UNIX自有其解决的办法,大家知道,一般CPU都是以""为单位来分配内存空间的,每一个页都是实际物理内存的一个映像,象INTELCPU,其一页在通常情况下是 4086字节大小,而无论是数据段还是堆栈段都是由许多""构成的,fork函数复制这两个段,只是"逻辑"上的,并非"物理"上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的""从物理上也分开。系统在空间上的开销就可以达到最小。
下面演示一个足以"搞死"Linux的小程序,其源代码非常简单:
void main()
{
for( ; ; ) fork();
}
这个程序什么也不做,就是死循环地fork,其结果是程序不断产生进程,而这些进程又不断产生新的进程,很快,系统的进程就满了,系统就被这么多不断产生的进程"撑死了"。当然只要系统管理员预先给每个用户设置可运行的最大进程数,这个恶意的程序就完成不了企图了。

2.2.2 exec( )函数族

exec函数族包括6个函数:

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, const char *envp[]);
int execv(const char *path, const char *argv[]);
int execve(const char *path, const char *argv[], const char *envp[];

int execvp(const char *file, const char *argv[]);

参数说明:
execl的第一个参数是包括路径的可执行文件,后面是列表参数,列表的第一个为命令path,接        着为参数列表,最后必须以NULL结束。
execlp的第一个参数可以使用相对路径或者绝对路径。
execle,最后包括指向一个自定义环境变量列表的指针,此列表必须以NULL结束。
execv,v表示path后面接收的是一个向量,即指向一个参数列表的指针,注意这个列表的最后        一项必须为NULL。
execve,path后面接收一个参数列表向量,并可以指定一个环境变量列表向量。
execvp,第一个参数可以使用相对路径或者绝对路径,v表示后面接收一个参数列表向量。
exec被调用时会替换调用它的进程的代码段和数据段(但是文件描述符不变),直接返回到调用它的进程的父进程,如果出错,返回-1并设置errno。

     此程序从终端读入命令并执行之,执行完成后,父进程继续等待从终端读入命令。熟悉DOSWINDOWS系统调用的朋友一定知道DOS/WINDOWS也有exec类函数,其使用方法是类似的,但DOS/WINDOWS还有spawn类函数,因为DOS是单任务的系统,它只能将"父进程"驻留在机器内再执行"子进程",这就是spawn类的函数。WIN32已经是多任务的系统了,但还保留了spawn类函数,WIN32中实现spawn函数的方法同前述 UNIX中的方法差不多,开设子进程后父进程等待子进程结束后才继续运行。UNIX在其一开始就是多任务的系统,所以从核心角度上讲不需要spawn类函数。
在这一节里,我们还要讲讲system()和popen()函数。system()函数先调用fork(),然后再调用exec()来执行用户的登录 shell,通过它来查找可执行文件的命令并分析参数,最后它么使用wait()函数族之一来等待子进程的结束。函数popen()和函数 system()相似,不同的是它调用pipe()函数创建一个管道,通过它来完成程序的标准输入和标准输出。这两个函数是为那些不太勤快的程序员设计的,在效率和安全方面都有相当的缺陷,在可能的情况下,应该尽量避免。

2多进程编程实践

 在开发大型项目的时候如果用单一进程执行程序效率是非常低的,我们可以同时创建多个进程来完成一个任务,这样程序的执行效率将大大提高,在Linux下对进程的操作主要以下接口:

  l 常见进程控制函数
  – fork  – 创建一个新进程
  – exec  – 用一个新进程去执行一个命令行
  – exit  – 正常退出进程
  – abort – 非正常退出一个进程
  – kill  – 杀死进程或向一个进程发送信号
  – wait  – 等待子进程结束
  – sleep – 将当前进程休眠一段时间
  – getpid – 取得进程编号
  – getppid – 取得父进程编号
  2、进程的的运行状态
  – 新建(new) – 正在创建
  – 运行(running) – 正在执行指令
  – 阻塞(blocked) – 等待像I/O这样的事件
  – 就绪(ready) – 等待分配处理器
  – 完成(done) – 结束
  ps查看进程状态
  PS显示状态
  – D 不可中断睡眠 (通常是在IO操作)
  – R 正在运行或可运行(在运行队列排队中)
  – S 可中断睡眠 (在等待某事件完成)
  – T Stopped, either by a job control signal or because it is being traced.
  – W 正在换页(2.6.内核之前有效)
  – X 死进程 (should never be seen)
  – Z 僵尸
  3. 进程调度
  -- CPU 一个时刻只能运行一个程序。在操作系统实现的多进程,看起来好象在同时运行多个程序。实际是是OS 的制造的假象
  -- 像一个人只有一双手,但是可以同时操作N 个桔子一样。这牵涉到一个调度的问题(schedule )。
  -- 在Linux 中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(ProcessControl Block,简称PCB)。PCB中包含了很多重要的信息,供系统调度和进程本身执行使用.
  -- 在调度时,操作系统不断进行上下文切换(context switch),即将一个进程从运行状态退出,并运行另一个进程。
  4. 进程的创建
  l 在Linux 中,创造新进程的方法只有一个:fork(), 创建子进程, 其它调用system,exec 最
  后也是调用fork.
  – pid_t fork();
  – shell 执行一个命令相当于调用了 fork
  当一个进程调用了fork 以后, 系统会创建一个子进程. 这个子进程和父进程不同的地方只有他的进程ID 和父进程ID, 其他的都是一样. 就象符进程克隆(clone) 自己一样
  – 参考代码.fork_test.c
  eg: 创建链式进程
  **************************************************************
  # include <unistd.h>
  # include <sys/type>
  int main(void)
  { 
       pid_t pid;
  int i;
  for(i = 0; i < 3; i++)
  {
  pid = fork();
  if(pid != 0)
  { break;}
  }
  sleep(5);
  return 0;
  }
  **************************************************************
  5. 进程的退出
  exit,_exit 或者main 函数里return 来设置进程退出值.
  为了得到这个值Linux定义了几个宏来测试这个返回值.
  WIFEXITED:判断子进程退出值是非0
  WEXITSTATUS:判断子进程的退出值(当子进程退出时非0).
  WIFSIGNALED:子进程由于有没有获得的信号而退出.
  WTERMSIG:子进程没有获得的信号号(在WIFSIGNALED 为真时才有意义).
  在shell用$?做同样事情.
  exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件
  的打开情况,把文件缓冲区中的内容写回文件,就是清理I/O缓冲
  6. 进程的阻塞
  一旦子进程被创建,父子进程一起从fork 处继续执行,相互竞争系统的资源.有时候我们希望子进程继续执行,而父进程阻塞直到子进程完成任务.这个时候我们可以调用wait 或者waitpid 系统调用.
  – pid_t wait(int *stat_loc);
  pid_t waitpid(pid_t pid,int *stat_loc,int options);
  wait 系统调用会使父进程阻塞直到一个子进程结束或者是父进程接受到了一个信号.如果没有父进程没有子进程或者他的子进程已经结束了wait 回立即返回.成功时(因一个子
  进程结束)wait将返回子进程的ID,否则返回-1,并设置全局变量errno.stat_loc是子进程的退出状态.子进程调用,从本质上讲,系统调用waitpid 和wait的作用是完全相同的,    但waitpid 多出了两个可由用户控制的参数pid 和options,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:
  – static inline pid_t wait(int * wait_stat) { return waitpid(-1,wait_stat,0); }
  7. 进程间同步
  进程同步就是要协调好2 个以上的进程,使之以安排好地次序依次执行,比如进程间同步有多种方法,其中用wait是一种方法之一
  – wait 只能用于有亲戚关系的进程之间进行同步
  1.管道:速度慢,容量有限,只有父子进程能通讯
  2.FIFO:任何进程间都能通讯,但速度慢
  3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
  4.信号量:不能传递复杂消息,只能用来同步
  5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。
  8、exec 相关函数
  int execl(const char * path,const char * arg,....);
  execl()用来执行参数path 字符串所代表的文件路径,接下来的参数代表执行该文件时传递过去的     argv(0)、argv[1]……,最后一个参数必须用空指针(NULL)作结束。
  eg:
  #include<unistd.h>
  main()
  {
  execl(“/bin/ls”,”ls”,”-al”,”/etc/passwd”,(char * )0);
  }
  int execv (const char * path, char * const argv[ ]);
  execv()用来执行参数path 字符串所代表的文件路径,与execl()不同的地方在于execve()只需两个参数,第二个参数利用数组指针来传递给执行文件。
  9、  System 函数
  system 是直接用一个字符串执行一个命令
  – system(“ls –l”)
  – system(“ifconfig eth0 192.168.0.104”);
  – 定义:int system(const char * c m d s t r i n g) ;
  int system(const char * string);
  system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string 来执行参数string 字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT 和
SIGQUIT 信号则会被忽略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

山西茄子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值