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机制来实现信息的输出
相关文章推荐
- DE1-SOC入门之Linux开发环境搭建
- Linux驱动开发-13、平台总线驱动模型
- Linux下的ip命令,除了ifconfig还有很多
- Linux驱动开发-12、总线设备驱动模型
- RHEL/CentOS 6.x 系统服务详解
- Linux驱动开发-11、设备阻塞访问-等待队列
- 慕课linux学习笔记(一)centOS的安装
- Linux进程间通信方式--本地socket
- Linux命令学习手册-od命令
- linux学习之八---Linux进程基础知识
- linux卸载驱动时 Resource temporarily unavailable
- Linux学习网站
- Linux文件目录详解
- linux 编译ffmpeg 支持x264, x265
- 《Linux内核分析》第七周: 可执行程序的装载
- [Zedboard Linux系统移植]-从MACHINE_START開始
- ubuntu linux基础与树莓派使用心得
- LINUX 用户及权限之我见
- centos 6.3最小化安装,无法上网解决方法
- linux server, windows SQL server 配置