和文件一样,进程是Unix系统最基本的抽象之一。
一、进程ID
每一个进程都有一个唯一的标示,进程ID。虽然进程ID是唯一的,但进程终止后,id会被其他进程重用。许多UNIX都提供了延迟重用的功能,以防止新进程被误认为是旧进程。
有一些特殊的进程:
id为0的进程--idle进程或者叫做swapper,通常是一个调度进程。
id为1的进程--内核booting之后执行的第一个进程。init进程一般执行的是init程序。
Linux通常尝试执行以下init程序:
1、/sbin/init: 偏向、最有可能是init程序的地方。
2、/etc/init: 另一个很有可能是init程序的地方。
3、/bin/init: 有可能是init进程的地方。
4、/bin/sh:如果内核找不到init进程,就执行该bourne shell。
init进程是一个用户级进程,但是需要执行者有超级用户权限。
二、获得进程ID和父进程的ID
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pit_t getppid(void);
pid_t是个抽象类型,在linux中pid_t一般是一个int类型,在<sys/types.h>定义。但把pid_t当做int类型,不具有可移植性。
printf("My pid=%d/n",getpid());
printf("Parent's pid=%d/n",getppid());
我们可以把pid_t比较安全的当做int类型,虽然这违反了抽象类型的意图和可移植性。
三、创建一个进程fork
一个已经存在的进程可以通过fork创建其他进程:
#include <unistd.h>
pid_t fork(void);
新创建的进程被称为子进程,这个函数被调用一次但是被返回两次。在子进程返回0,父进程返回子进程的t_pid。
之所以在父进程返回子进程的id,是由于父进程可以有多个子进程,并且没有提供获得所有子进程的方法。在子进程
中返回0,是因为子进程只有一个父进程,并且可以通过getppid获得。
fork被调用之后,父进程和子进程都开始执行fork之后的程序语句。子进程是父进程的一个拷贝,拷贝了父进程的数据
空间,堆,栈,它们共享text段。
当前fork的实现并不是拷贝父进程的数据、堆、栈,而是使用了copy-on-write技术,这是因为fork之后通常会调用exec。
如果它们修改了这些区域,那么内核就会把相应的那部分内存进行拷贝。
子进程和父进程有以下不同:
1)进程id不同
2) fork的返回值不同
2)子进程的父进程id设置为父进程的id,它们的父进程不同
3)子进程的资源统计归为0
4)任何pending的signals被清空,不会被子进程继承
5)任何获得的文件锁都不会被子进程继承。
相同的:
1)打开文件
2)real user ID,real group ID,effective user ID,effective group ID
3)进程的group ID
4)Session ID
5)控制终端
6)set-user-ID和set-group-ID
7)当前工作目录
8)Root目录
9)文件mode创建的掩码
10)信号掩码和dipositions
11)打开文件的close-on-exec flag
12)环境变量
13)附加进去的共享内存段
14)内存映像
15)资源限制
如果失败返回-1。
例子:
pid_t pid;
pid = fork();
if(pid > 0){
printf("I am the parent of pid=%d/n",pid);
}else if(!pid){
printf("I am the baby!/n");
}else if(pid == -1){
perror("fork");
}
fork经常和exec在一起使用:
Cpp代码 复制代码
pid_t pid;
pid = fork();
if(pid == -1)
perror("fork");
if(!pid){
const char *args[] = {"windlass",NULL};
int ret;
ret = execv("/bin/windlass",args);
if(ret == -1){
perror("execv");
exit(EXIT_FAILURE);
}
}
使用fork的场景:
1)当一个进程想复制自己以便父子进程可以同时执行不同部分的代码。比如一个网络的服务器,
父进程等待从客户端发来的请求,当请求到来时,父进程调用fork,让新创建的子进程处理请求,
父进程继续等待客户端发来的请求。
2)当一个进程想执行不同的程序。比如shell,子进程在fork返回之后执行了exec
四、vfork
在copy-on-write技术使用之前,Unix的设计者认为fork之后执行exec浪费了地址空间的拷贝,BSD开发者
实现了vfork系统调用:
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork和fork行为一样,除了子进程要立即调用exec函数或者执行exit退出。vfork系统调用避免了地址
空间和页表的拷贝,通过挂起父进程直到子进程终止或者执行一个二进制的进程映像。
vfork的例子:
#include <sys/types.h>
#include <unistd.h>
int glob = 6;
int main(){
int var;
pid_t pid;
var = 88;
printf("before vfork/n");
if((pid = vfork()) < 0){
perror("vfork");
return 1;
}else if(pid == 0){
glob++;
var++;
_exit(0);
}
printf("pid=%d",glob=%d,var=%d/n",getpid(),glob,var);
exit(0);
}
输出:pid=2903,glob=7,var=89.在子进程里面增加变量会反映到父进程中,因为他们共享同一进程空间。
五、终止进程
POSIX和C89都定义了终止当前进程的标准函数:
#include <stdlib.h>
void exit(int status);
调用exit会执行一些关闭操作步骤,然后指示内核终止进程。
status表示进程终止的状态。EXIT_SUCESS和EXIT_FAILURE被定义为一种可移植的方式来表示成功和失败。
在终止之前要做一些关闭的步骤:
1)调用任何注册在atexit()和on_exit()的方法,和注册的顺序相反。
2)flush所有打开的I/O流
3)删除进程由tmpfile()函数创建的临时文件。
执行完这些步骤之后,调用_exit(),让内核来处理剩余的终止操作:
#include <unistd.h>
void _exit(int status);
当进程终止后,内核清空了进程申请的所有资源。
程序可以直接调用_exit,但是很多程序需要执行一些清理操作,比如flush标准输出流。但是vfork用户必须
使用_exit终止,因为父子进程共享一个地址空间,exit执行一些I/O清理工作可能把父进程的文件描述流关闭,
导致父进程I/O失败。
六、atexit和on_exit
1、atexit:
注册在进程终止之前回到的函数:
#include <stdlib.h>
int atexit(void (*function)(void));
如果进程通过exit或者从main返回终止,则会调用注册到atexit的方法。如果进程调用exec函数,注册函数则
被清空(因为这些函数不在新的进程空间存在)。如果信号终止了进程,则注册的函数不会被调用。
被注册的函数按照逆序执行,如果被注册的函数执行了exit,则会导致无穷递归,如果想提前终止需要使用
_exit。atexit支持至少ATEXT_MAX个注册函数,这个值可以通过sysconf得到。
long atexit_max;
atexit_max = sysconf(_SC_ATEXIT_MAX);
printf("atexit_max=%ld/n", atexit_max);
atexit例子:
#include <stdio.h>
#include <stdlib.h>
void out(void){
println("atexit() succeed!/n");
}
int main(){
if(atexit(out))
fprintf(stderr,"atexit() failed!/n");
return 0;
}
2、on_exit:
on_exit和atexit等价,Linux glibc实现了它:
#include <stdlib.h>
int on_exit (void (*function)(int , void *), void *arg);
但是注册的签名函数不同,原型是:
void my_func(int status,void *args);
status是传到exit的或者从main返回的值。args是传到on_exit的第二个参数。Solaris已经不再支持on_exit,
所以最好使用atexit().
七、等待子进程终止——wait和waitpid
僵尸进程
当子进程退出的时候,内核会向父进程发送SIGCHLD信号,子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止)子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。
A child that terminates, but has not been waited for becomes a "zombie". The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child. As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes. If a parent process terminates, then its "zombie" children (if any) are adopted by init(8), which automatically performs a wait to remove the zombies.
父进程查询子进程的退出状态可以用wait/waitpid函数。
如何避免僵尸进程
当一个子进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行或者父进程调用了wait/waitpid才告终止。
进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的wait/waitpid调用使用。它将称为一个“僵尸进程”。
调用wait或者waitpid函数查询子进程退出状态,此方法父进程会被挂起(waitpid可以设置不挂起)。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。也可以不忽略SIGCHLD信号,而接收在信号处理函数中调用wait/waitpid。
而在运维中常用的手段是杀死父进程,这样子进程会由init进程接管,由它来清理子进程的状态——init周期性的调用wait来回收僵尸。
wait是waitpid的特例
当一个进程终止之后,内核向父进程发送一个SIGCHLD。默认这个信号被忽略,进程可以通过singnal()或者sigaction()系统调用来处理这个信号。父进程希望得到子进程终止的更多信息,比如返回值,甚至显式地等待这个事件的到来,这就是wait或者waitpid,它们可以做:
1)阻塞,如果子进程仍然在执行。
2)立即返回,包含子进程的终止状态,如果一个子进程终止,等待它的终止状态被获取。
3)返回错误,如果它没有子进程。
1. wait & waitpid的区别:
1)wait会阻塞调用者,直到一个子进程终止,而waitpid有一个设置不阻塞的选项。
2)waitpid不一定是等待第一个终止的子进程,他有一些选项来控制进程的等待。
2. wait & waitpid的相同点:
The wait() system call suspends execution of the calling process until one of its children terminates.
The call wait(&status); is equivalent to waitpid(-1, &status, 0);
3. wait(&status):
#include <sys/wait.h>
pid_t wait(int *statloc);
子进程的结束状态会保存在statloc指针中,如果不关心结束状态,可以直接传一个NULL。POSIX指定了通过一系列宏来获得终止的状态。
#include <sys/wait.h>
int WIFEXITED(status); 子进程正常结束(例如子进程调用了exit(100)而产生...信号将其终止),返回非零值(代表true)
int WEXITSTATUS(status);通过WEXITSTATUS得到返回值的低8位
int WIFSIGNALED(status); 子进程因捕获信号而不正常终止(例如因为子进程调用了abort()而产生SIGABR信号将其终止),返回一个非零值(代表true)
int WTERMSIG(status); 可以通过WTERMSIG返回信号的号
int WIFSTOPPED(status); 如果进程被终止,返回一个非零值(代表true)
int WSTOPSIG(status); 通过WSTOPSIG来获得导致子进程停止的信号
int WIFCONTINUED(status); 如果状态是由已经被continued子进程返回,返回一个非零值(代表true)
例子:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void){
int status;
pid_t pid;
if(! fork()) {
return 1;
}
pid = wait(&status);
if(pid == -1)
perror("wait");
printf("pid=%d/n",pid);
if(WIFEXITED(status))
printf("Normal termination with exit status=%d/n",WEXITSTATUS(status));
if(WIFSIGNALED(status))
printf("Killed by signal=%d/n",WTERMSIG(status));
if(WIFSTOPPED(status))
printf("Stopped by signal=%d/n",WSTOPSIG(status));
if(WIFCONTINUED(status))
printf("Continued/n");
return 0;
}
4. waitpid(pid, &status, options):
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
1) pid参数指定了要等待的进程id:
< -1:等待任意一个绝对值和进程group id相等的进程。
-1:等待任意一个子进程,和wait一样
0:等待和当前进程相同组的任一子进程。
> 0: 等待该进程id的子进程
2) statloc保存子进程的结束状态,和wait一样,在此略去。
3) option 可以通过OR连接下列选项:
WNOHANG:不阻塞,如果没有匹配的子进程也直接返回。
WUNTRACED:如果被设置,WIFSTOPPED也会被设置。允许更一般的作业控制。
WCONTINUED:如果被设置,WIFCONTINUED也被设置。和WUNTRACED一起,对于实现一个shell很有用。
例子:
int status;
pid_t pid;
pid = waitpid (1742, &status, WNOHANG);
if (pid == -1)
perror ("waitpid");
else {
printf ("pid=%d/n", pid);
if (WIFEXITED (status))
printf ("Normal termination with exit status=%d/n",WEXITSTATUS (status));
if (WIFSIGNALED (status))
printf ("Killed by signal=%d%s/n",WTERMSIG (status),WCOREDUMP (status) ? " (dumped core)" : "");
}
八、exec函数
当进程调用exec时,当前进程的镜像被由path标定的程序加载到内存中代替。下面是exec一族函数:
#include <unistd.h>
int execl(const char *path,const char *arg,...);
int execv(const char *path,char * const argv[]);
int execle(const char *path,const char *arg,...);
int execve(const char *path,char *const argv[], char * const envp[]);
int execlp(const char *path,const char *arg,...);
int execvp(const char *path, char *const argv);
区别:1)前四个参数path代表路径名,后两个代表文件名(不包含路径信息)。
如果文件名中包含反斜线,作为一个路径名
如果否则从PATH环境变量中找可执行的文件。
如果execlp或者execvp找到了可以执行的文件,但是不是机器可执行的,那么就假设这是一个shell脚本,
调用/bin/sh执行该shell脚本。
2)execl、execle、execlp使用的是参数列表,需要以NULL终止,而其他的几个带v的是一个数组参数,
参数数组也要以NULL终止。
3)execle、execve传递环境列表到新的程序中。
这里面只有execve是系统调用,其他都是函数。
例子:
int ret = execl("/bin/vi","vi",NULL);
if(ret == -1)
perror("execl");
下面一个例子,要使用vi打开以及文件编辑:
int ret = execl("/bin/vi","vi","/home/fuliang/books.txt",NULL);
if(ret == -1)
perror("execl");
成功调用execl之后,改变的不仅是地址空间和进程映像,而且还改变进程一下属性:
1)任何pending signals都被丢弃。
2)任何要捕获的信号都置成默认的行为。因为信号处理函数不在该进程的地址空间了。
3)任何的内存锁都被释放。
4)很多线程属性被设置为默认值。
5)任何和进程内存相关,包括内存映射文件,都被丢弃。
6)任何在用户空间存在的包括C语言库,比如atexit的行为被丢弃。
没有改变的进程属性:
进程pid,父进程id,优先级,进程所属的用户和组。
execvp例子:
const char *args[] = {"vi","/home/fuliang/books.txt",NULL};
int ret = execvp("vi",args);
if( ret == -1 )
perror("execvp");
失败返回-1,并设置errno:
1)E2BIG:参数列表或者环境变量envp太长。
2)EACCESS:进程没有搜索path的权限,path不是一个普通文件,目标文件不可执行,文件系统被mounted为不可读。
3)EFAULT:所给的指针不合法。
4)EIO:底层的I/O发生错误。
5)EISDIR:path或者解释器是一个目录。
6)ELOOP:解析path的时候遇到太多的软链。
7)EMFILE:调用进程超过了打开文件的限制。
8)ENFILE:系统打开文件数目超过限制。
9)ENOENT:path不存在,或者以来的共享库不存在。
10)ENOEXEC:目标path是一个不合法的二进制文件或者是不同的机器架构。
11)ENOMEM:没有足够的内核内存来执行新的程序
12)ENOTDIR:path不是一个目录.
13)ETXTBSY:目标文件正被其他的进程写
九、Launching and Waiting for a New Process
ANSI C和POSIX都定义了一个接口,他结合了创建一个进程并且等待它的结束:
#include <stdlib.h>
int system(const char *command);
system一般用于执行一个简单的工具或者shell脚本。
成功返回命令的执行状态,如果command是NULL,则返回非0整数。
在执行command命令式,SIGCHILD被阻塞,SIGINT和SIGQUIT被忽略。忽略SIGINT和SIGQUIT有几个含义,
尤其system在一个循环里被执行,这时你需要保证程序检查子进程的状态。
do{
int ret = system("ls -l");
if(WIFSIGNALED(ret) && WTFERMSIG(ret) == SIGINT || WTERMSIG(ret) == SIGQUIT))
break;
}while(1);
使用fork、waitpid简单实现system:
int my_system(const char *cmd){
int status;
pid_pid;
pid = fork();
if(pid == -1);
return -1;
else if(pid == 0){
const char *argv[4];
argv[0] = "sh";
argv[1] = "-c";
argv[2] = cmd;
argv[3] = NULL;
execv("/bin/sh",argv);
exit(-1);
}
if( waitpid(pid, &status,0) == -1)
return -1;
else if(WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}
参考:
1、 Linux进程管理 http://blog.csdn.net/flyqwang/article/details/6039937
2、《Linux system programming》
3、《Unix system programming》
4、《Advanced Programming in the Unix Environment》
5、 僵尸进程、wait、waitpid http://blog.csdn.net/jnu_simba/article/details/8931908
相关推荐
### LINUX进程管理实验知识点解析 #### 一、进程与程序的区别 在进行LINUX进程管理实验之前,首先需要理解进程与程序之间的区别。程序是指令的集合,是静态的,而进程则是程序的一次动态执行过程,具有生命周期,...
在Unix/Linux系统中,进程池(Process Pool)是一种高效的进程管理机制,广泛应用于服务器和后台服务,例如银行系统。进程池的基本思想是预先创建一组进程,这些进程在池中待命,等待处理到来的任务,而不是每次有新...
Linux 进程管理命令 Linux 进程管理命令是 Linux 操作系统中用于管理进程的命令,它们是 Linux 系统管理员的基本工具。这些命令可以用来查看、管理和控制进程,从而确保系统的稳定运行。 1. 程序和进程 在 Linux ...
Linux 进程管理实验 本实验的目的是了解 Linux 进程管理的基本概念和相关系统调用,掌握父进程和子进程的内存映像,实现软中断通信和进程的管道通信。 一、基本概念 * 进程管理:是操作系统中负责管理进程的模块...
linux进程管理分析
Linux 进程基本管理是计算机操作系统中一个非常重要的概念,它涉及到进程的创建、管理和控制。在 Linux 环境下,进程是操作系统中一个基本的执行单元,每个进程都有其自己的虚拟地址空间、打开的文件描述符、信号...
Linux 进程管理是 Linux 操作系统中的一项重要功能, Linux 是一个多任务多用户操作系统,每一个进程都具有一定的功能和权限,它们都运行在各自独立的虚拟地址空间。进程是系统资源分配的基本单位,也是使用 CPU ...
在Linux操作系统中,进程管理是系统管理的核心部分,它涉及到计算机如何有效地运行多个程序并确保它们协同工作。这个“Linux下进程管理实验”旨在帮助我们深入理解进程的概念,以及它们与程序之间的区别,同时探讨...
四大命令助你玩转Linux进程管理 命令 Linux 进程管理
在Linux服务器管理中,进程管理是一项基础且至关重要的任务,它涉及到系统性能监控、资源调度以及问题排查。本文将深入探讨Linux的进程管理方法,特别是如何使用`ps`和`top`这两个命令来查看和监视进程状态。 首先...
Linux进程管理和网络管理是Linux系统运维中非常重要的两个方面,涉及到系统性能监控、服务维护、故障排查等多个环节,是系统管理员必备的技能之一。 一、Linux进程管理 1. 进程概念 进程是计算机中的程序关于某...
### Linux 进程管理知识点详解 #### 进程组成 - **正文段**:这部分包含了进程需要执行的程序代码,具体地,它描述了进程所要实现的功能逻辑。 - **用户数据段**:用于存储正文段执行时所需的数据以及工作区,比如...
Linux进程管理实验中涉及的核心知识点包括Linux操作系统中进程的概念、进程控制块(PCB)的结构、进程状态转换、fork()系统调用的工作原理以及相关内核函数。下面将详细解释这些概念和技术点。 Linux中的进程是一个...
模拟 LINUX 模拟进程管理 本系统实现了进程调度、进程控制、进程同步等功能,模拟了 LINUX...本系统是一个功能强大且实用的模拟 LINUX 模拟进程管理系统,能够满足多种应用场景的需求,并且可以作为教学和研究的工具。
1、linux进程管理的模块组织框架 2、相关数据结构。 3、进程调度原则,调度算法,。 4、进程的创建和运行管理。 5、进程间通讯。 6、更多的技术 进程调度和中断处理交接 进程管理涉及的内核机制:bottom-half处理...
Linux进程管理及作业控制 Linux作为一个多用户多任务的操作系统,能够同时执行多个任务,系统上同时运行着多个进程,正在执行的一个或多个相关进程称为一个作业。使用作业控制,用户可以同时运行多个作业,并在需要...
总的来说,这个压缩包提供的源码是一个实践性的Linux进程管理工具,它涵盖了进程创建、控制、信息获取以及定时任务等多个关键知识点,对于学习和理解Linux系统编程具有很高的价值。通过深入研究这些源码,开发者不仅...
除了`task_struct`之外,Linux还使用了其他一些数据结构来辅助进程管理: - **task数组**:`task`是一个固定大小的数组,每个元素都是指向`struct task_struct`的指针。数组大小默认为512,这限制了系统同时能够...
Linux进程管理代码分析
(1) 熟悉 linux 常用命令: pwd, useradd, passwd, who, ps, pstree, kill, top, ls, cd, mkdir,rmdir, cp, rm, mv, cat, more, grep 等。...(5) 利用 linux 的共享内存通信机制实现两个进程间的通信: