Linux系统编程学习笔记(五)进程管理1

sunln00 2010-04-29

进程管理(一)

和文件一样,进程是Unix系统最基本的抽象之一。

1、进程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进程,就执行该bourneshell。

init进程是一个用户级进程,但是需要执行者有超级用户权限。

2、获得进程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类型,虽然这违反了抽象类型的意图和可移植性。

2、创建一个进程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)realuserID,realgroupID,effectiveuserID,effectivegroupID

3)进程的groupID

4)SessionID

5)控制终端

6)set-user-ID和set-group-ID

7)当前工作目录

8)Root目录

9)文件mode创建的掩码

10)信号掩码和dipositions

11)打开文件的close-on-execflag

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在一起使用:

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。

3、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.在子进程里面增加变量会反映到父进程中,因为他们共享同一进程空间。

4、终止进程:

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失败。

5、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等价,Linuxglibc实现了它:

#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().

6、等待子进程终止:

当一个进程终止之后,内核向父进程发送一个SIGCHLD。默认这个信号被忽略,进程可以通过singnal()或者

sigaction()系统调用来处理这个信号。父进程希望得到子进程终止的更多信息,比如返回值,甚至显示的等

待这个事件的到来,这就是wait或者waitpid,它们可以做:

1)阻塞,如果子进程仍然在执行。

2)立即返回,包含子进程的终止状态,如果一个子进程终止,等待它的终止状态被获取。

3)返回错误,如果它没有子进程。

#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

这两个函数的不同之处:

1)wait会阻塞调用者,直到一个子进程终止,而waitpid有一个设置不阻塞的选项。

2)waitpid不是等待第一个终止的子进程,他有一些选项来控制进程的等待。

如果一个进程终止,其父进程没有等待他终止,它就成为僵尸进程,僵尸进程的父进程会置为init,

进程,init周期性的调用wait来回收僵尸。

子进程的结束状态会保存在statloc指针中,如果不关心结束状态,可以直接传一个NULL。

POSIX指定了通过一系列宏来获得终止的状态。

#include <sys/wait.h>

int WIFEXITED(status);
int WIFSIGNALED(status);
int WIFSTOPPED(status);
int WIFCONTINUED(status);

int WEXITSTATUS(status);
int WTERMSIG(status);
int WSTOPSIG(status);
int WCOREDUMP(status);

WIFEXITED:如果子进程正常终止,返回true。可以通过WEXITSTATUS得到参数的低8位。

WIFSIGNALED:如果是信号导致子进程不正常终止,返回true。可以通过WTERMSIG返回信号的号。

一些UNIX实现定义了WCOREDUMP宏,如果进程dumpcore来响应信号。

WIFSTOPPED:如果进程被停止,返回true,通过WSTOPSIG来获得导致子进程停止的信号。

WIFCONTINUED:如果状态是由已经被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%s\n",WTERMSIG(status),WCOREDUMP(status));
	if(WIFSTOPPED(status))
		printf("Stopped by signal=%d\n",WSTOPSIG(status));
	if(WIFCONTINUED(status))
		printf("Continued\n");

	return 0;
}

waitpid比wait功能更加强大。

pid参数指定了要等待的进程id:

<-1:等待任意一个绝对值和进程groupid相等的进程。

-1:等待任意一个子进程,和wait一样

0:等待和当前进程相同组的任一子进程。

>0:等待该进程id的子进程

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)" : "");
}

7、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;

ret = execl("/bin/vi","vi",NULL);
if(ret == -1)
	perror("execl");

下面一个例子,要使用vi打开以及文件编辑:

int ret;

ret = execl("/bin/vi","vi","/home/fuliang/books.txt",NULL);
if(ret == -1)
	perror("execl");

成功调用execl之后,改变的不仅是地址空间和进程映像,而且还改变进程一下属性:

1)任何pendingsignals都被丢弃。

2)任何要捕获的信号都置成默认的行为。因为信号处理函数不在该进程的地址空间了。

3)任何的内存锁都被释放。

4)很多线程属性被设置为默认值。

5)任何和进程内存相关,包括内存映射文件,都被丢弃。

6)任何在用户空间存在的包括C语言库,比如atexit的行为被丢弃。

没有改变的进程属性:

进程pid,父进程id,优先级,进程所属的用户和组。

execvp例子:

const char *args[] = {"vi","/home/fuliang/books.txt",NULL};
int ret;

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:目标文件正被其他的进程写

8、LaunchingandWaitingforaNewProcess:

ANSIC和POSIX都定义了一个接口,他结合了创建一个进程并且等待它的结束:

#include <stdlib.h>

int system(const char *command);

system一般用于执行一个简单的工具或者shell脚本。

成功返回命令的执行状态,如果command是NULL,则返回非0整数。

在执行command命令式,SIGCHILD被阻塞,SIGINT和SIGQUIT被忽略。忽略SIGINT和SIGQUIT有几个含义,

尤其system在一个循环里被执行,这时你需要保证程序检查子进程的状态。

do{
	int ret;
	
	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、《Linuxsystemprogramming》

2、《Unixsystemprogramming》

3、《AdvancedProgrammingintheUnixEnvironment》

相关推荐

HappinessCat / 0评论 2020-06-16
Linuxer / 0评论 2016-08-10