您的位置:首页 > 运维架构 > Linux

linux进程基础

2016-04-04 15:57 411 查看

什么是进程

  一个运行中的程序即为一个进程,是计算机程序关于某个数据集合的运行活动,是一个动态的概念。

进程可以申请和拥有系统资源,是资源分配的基本单位。

查看进程

  在linux中可以用ps命令查看当前系统中的所有进程



PID:进程ID

PPID:父进程ID

  在linux中,每个进程都有一个非负整数表示的唯一进程ID,用于标志进程。

  因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。如应用程序有时候把进程ID作为名字的一部分来创建一个唯一的文件名。

进程ID的分配方法

  在大多数Linux/Unix系统中,生成一个进程ID方法是:从0开始依次连续分配,一直到可以分配的最大的进程ID(不同的系统,这个最大值是不一样的,比如有些Linux系统是65536)。

  一旦到达最大值,重新从某个值(不同的系统,这个值也是不一样的)开始依次连续查找那些还没有被使用的ID。部分系统可能用其他方法来分配进程ID,比如随机分配一个进程ID。无论用什么方法分配进程ID,系统都需要保证每个进程ID是独一无二的。

  由于进程ID的数量是有限的(一般为0~65535),因此应尽量避免产生新进程,并且当进程执行完成时,要注意释放资源

怎样创建进程

#include <unistd.h>
pid_t fork(void);
//返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1


  在linux中,一个进程可以通过调用fork()函数来创建一个新进程,原来的进程称为父进程,新进程称为子进程。

  fork()函数被调用一次,但返回两次。其中父进程返回子进程的进程ID,子进程返回0。子进程可以通过getppid()来获得父进程的进程ID。这样父、子进程都能知道对方的信息了。

  

  fork怎样实现返回两次

  在同一时刻,CPU只能调度一个进程,即在任何时刻都只有一个进程在运行。当一个进程的时间片结束后,操作系统就会调度另一个进程,将其进程上下文从进程表中读出,同时更新寄存器。

  fork()函数的返回值是放在eax寄存器中的,在调用fork()函数后,一个进程就会变为父子两个进程了,但是操作系统会先调度哪个进程却是未知的。当父进程先被调度时,就会先读入父进程的上下文,同时将子进程的进程ID写入寄存器中,因此对于父进程来说,fork的返回值就是子进程ID。

  如果调度子进程时,就会在读入子进程的上下文后将寄存器置0,这样看起来fork在子进程中的返回值就是0了。

  这样就使得fork()函数看起来好像返回了两次。

资源共享

  父进程和子进程继续执行fork之后的指令,子进程是父进程的副本。即子进程会获得父进程数据空间、堆、栈的副本。注意子进程拥有的是副本,父子进程并不共享这些存储空间,父子进程只共享正文段。

  即无论是全局变量还是局部变量,当fork出子进程后,父子进程就拥有两个完全独立的副本,修改时不会对另一个进程造成影响。

int glob=6;         //全局变量
int main(void){
int var=18;          //局部变量
pid_t pid;
if((pid=fork())<0){
//error;
}else if(pid==0){   //子进程,修改变量
glob++;
var--;
}else{              //父进程
sleep(2);       //父进程调用sleep,则会先调度子进程
}
printf("glob=%d,var=%d\n",glob,var);      //父子进程公共部分,两个进程都会执行到这里
return 0;
}


  最后输出结果为:

  glob=7,var=17 //子进程

  glob=6,var=18 //父进程

  可以发现两个进程对数据的修改是相互独立的,即在两个进程中都有一个副本。

COW(copy on write)

  由于在fork之后,经常会调用exec去执行另一个程序段,因此现在很多实现并不执行一个父进程数据段、堆、和栈的完全复制,而是采用COW技术。这些区域由父子进程共享,但是内核将它们的权限变为只读的,如果父、子进程中任何一个进程试图修改这些区域时,内核只为要修改的区域制作一个副本。

文件共享

  在调用fork之后,父进程所有打开的文件描述符都会被复制到子进程中,父子进程的每个相同的打开描述符共享一个文件表项



  文件表中有一个非常重要的字段:当前文件偏移量。因此这种共享方式使得父子进程对同一文件使用了一个文件偏移量。

  因此父子进程需要操作同一个描述符时,必须进行同步操作。同时父子进程在fork之后最好关闭各自不需要使用的描述符(只会使refcnt减一,不会真的关闭)。

  在网络通信中,如果只调用close只会使refcnt减一,并不一定会真的关闭socket,如果需要直接强制关闭socket,可以调用shutdown进行四次握手关闭。

僵尸进程&孤儿进程

  若一个进程已经终止,但是其父进程未对其进行善后处理(获取终止子进程的有关信息,释放占用的资源),则该进程被称为僵死进程(zombie)。可用ps -l命令查看,其状态为Z+。

  当一个进程的父进程终止时,该进程就会变为孤儿进程,然后被init进程领养。主要过程是,当一个进程终止时,内核逐个检查所有的活动进程,判断是否是终止进程的子进程。若是,则将该进程的父进程ID置为1(init进程的进程ID)。

  而init进程被编写为无论何时,只要有一个子进程终止就会调用wait函数取得终止状态进行处理。这样就避免了僵死进程。

  对于僵死进程,即使是通过kill命令也是不能杀死的,只能通过杀死其父进程,使其成为孤儿进程被init进程接收后再进行清理。

  僵死进程的主要危害是会一直占用进程ID,而进程ID是有限的。如果产生大量的僵死进程,将会导致进程ID耗尽。

处理终止进程

  进程终止时一个异步事件,当一个进程正常或异常终止时,内核就向其父进程发送一个SIGCHLD信号。父进程可以通过调用wait或waitpid函数获得子进程的终止状态

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
//成功则返回0,出错返回-1。当进程没有任何子进程时,出错
//statloc指针可以用来存放子进程的终止状态,若不关心终止状态,可将其置为NULL


父进程调用wait或waitpid后:

- 当所有子进程都在运行,则阻塞

- 如果有一个子进程已经终止,则取得该子进程的终止状态立即返回

- 如果没有任何子进程,则出错

  其中wait函数是阻塞的,而waitpid可以将选项设置为非阻塞。即如果在任意时刻调用wait函数,则可能导致进程阻塞。

  因此可以为进程安装信号处理函数,当收到一个SIGCHLD信号时,表明此时有子进程终止,再通过wait函数进行处理,则不会导致进程阻塞。

  因此我们可以通过调用wait函数来避免僵死进程。但是这种方法也并不安全。考虑如下场景:

  在client/server模式下,如果有多个client连接到一个server,则server会fork出多个子进程,分别处理每个client。假设此时一共有5个client正在与server进行通信,且这5个client同时调用close关闭连接,则5个子进程都会结束,内核会向它们的父进程发送5次SIGCHLD信号。

  但是如果在调用信号处理函数之前,所有信号都已经到达用户空间,则信号处理函数可能只会被调用1次,因此wait函数也只会获取到一个已经终止的子进程的状态,就会返回,也不会再进入这个信号处理函数。因此会导致系统中仍然存在4个僵死进程。

  因此我们最好通过调用waitpid函数而不是wait函数来避免僵死进程。

  我们可以通过循环调用waitpid函数,并将waitpid选项设置为WNOHANG,即设为非阻塞的。直到获得所有子进程的终止状态。

fork两次

  我们可以通过调用wait函数来避免僵死进程,但这种方法要求父进程一定要等待子进程终止。如果我们不要父进程等待子进程终止,也不要子进程一直处于僵死状态直到父进程终止。我们可以通过fork两次来实现。

int main(void){
pid_t pid;
if((pid=fork())==0){           //first child
if((pid=fork())>0){        //first child
exit(0);               //第一个子进程退出,则父进程可以立即获得其终止状态,不用一直等待。子子进程的父进程自动变为init,也不会僵死
}else{                     //second child
sleep(2);               //子子进程先调用sleep可以保证第一个子进程先执行exit,然后该进程就被init进程接收
XXXXX;
}
}else{                         //parent
waitpid(pid,NULL,0);       //等待第一个子进程结束,子进程已经结束,因此会立即返回。
}


  这样父进程不需要一直等待子进程终止,第二个子进程也已经被init进程接收,不会成为僵死进程了。

守护进程

  守护(daemon)进程是在后台运行且不与任何控制终端相关联的进程。

  守护进程的特征:

  守护进程一般作为服务器端程序,在后台一直运行,为系统提供服务。直到系统关闭时,才结束运行。

  守护进程一般是进程组的组长进程,并且是会话的首进程。

  

  守护进程的启动方法:

  守护进程有多种启动方法

  1)在系统启动阶段,由系统初始化脚本启动,如inetd、cron等守护进程进程

  2)许多网络服务器又inetd超级服务器启动,inetd监听网络请求,每当有一个请求到来时,就启动相应的服务器,如FTP、Telnet等

  3)由cron守护进程启动

  4)由at命令指定在某个时刻运行,当设定时间到来时,再通过cron进程启动对应的守护进程

  5)从用户终端启动,这样启动的守护进程必须由自身亲自脱离与控制终端的关联,从而避免与作业控制、终端会话管理、终端产生信号等任何不期望的交互。

  

  守护进程的编程规则:

  1)调用umask(0),打开所有权限,子进程会继承父进程的文件权限屏蔽字,这样可以避免创建文件时的权限限制

  2)调用fork创建子进程,并使子进程调用exit退出,这样子进程就继承了父进程的进程组ID

  3)调用setsid,创建一个新的会话,使得子进程满足(a)成为会话首进程,(b)成为进程组的组长进程,(c)不再拥有控制终端

  4)将工作目录改为根目录,以免影响可加载文件系统。或者也可以改变到某些特定的目录。

  5)关闭所有打开的文件描述符

  6)将标准输入、标准输出、标准错误输出重定向到dev/null,关闭守护进程与终端的交互  

  

  守护进程的消息输出:

  daemon进程既然与终端没有交互,也就不能通过printf输出信息了 。我们可以通过syslog机制来实现信息的输出

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: