进程是程序的执行实例,它是Linux的基本调度单位。一个进程由如下元素组成:
l 程序的当前上下文,即程序的当前执行状态;
l 程序的当前执行目录
l 程序访问的文件和目录
l 程序的访问权限,比如它的文件模式和所有权
l 内存和其他分配给进程的系统资源
内核使用进程来控制对CPU和其他系统资源的访问,并且使用进程来决定在CPU上运行哪个程序,运行多久以及采用什么特性运行它。内核的调度器负责在所有的进程间分配CPU执行时间,称为时间片(time slice),它轮流在每个进程分得的时间片用完后从进程那里抢回控制权。
OS会为每个进程分配一个唯一的整型ID,做为进程的标识号(pid)。进程除了自身的ID外,还有父进程ID(ppid),所有进程的祖先进程是同一个进程,它叫做init进程,ID为1,init进程是内核自举后的一个启动的进程。init进程负责引导系统、启动守护(后台)进程并且运行必要的程序。
进程的pid和ppid可以分别通过函数getpid()和getppid()获得。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("uid:%d gid:%d euid:%d egid:%d/n",
getuid(),getgid(),geteuid(),getegid());
return 0;
}
|
进程在运行过程中,必须具有一类似于用户的身份,以便进行进程的权限控制,缺省情况下,哪个登录用户运行程序,该程序进程就具有该用户的身份。例如,假设当前登录用户为gotter,他运行了ls程序,则ls在运行过程中就具有gotter的身份,该ls进程的用户ID和组ID分别为gotter和gotter所属的组。这类型的ID叫做进程的真实用户ID和真实组ID。真实用户ID和真实组ID可以通过函数getuid()和getgid()获得。
与真实ID对应,进程还具有有效用户ID和有效组ID的属性,内核对进程的访问权限检查时,它检查的是进程的有效用户ID和有效组ID,而不是真实用户ID和真实组ID。缺省情况下,用户的(有效用户ID和有效组ID)与(真实用户ID和真实组ID)是相同的。有效用户id和有效组id通过函数geteuid()和getegid()获得。
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("pid:%d ppid:%d/n",
getpid(),getppid());
return 0;
}
|
开始时登录用户为ghaha(uid为500):
shell>id
uid=500(ghaha) gid=500(ghaha) groups=500(ghaha)
编译生成可执行文件a.out,程序文件的属性可能为:
-rwxrwxr-x
1 ghaha ghaha 12132 Oct 7 09:26 a.out
执行结果可能为:
shell>a.out
uid:500 gid:500 euid:500 egid:500
现在将a.out的所有者可执行属性改为s
shell>chmod u+s a.out
shell>ll
-rwsrwxr-x
1 ghaha ghaha 12132 Oct 7 09:26 a.out
此时改另外一个用户gotter登录并运行程序a.out
shell>id
uid=502(gotter) gid=502(gotter) groups=502(gotter)
shell>a.out
uid:502 gid:502 euid:500 egid:502
可以看到,进程的有效用户身份变为了ghaha,而不是gotter了,这是因为文件a.out的访问权限的所有者可执行为设置了s的属性,设置了改属性以后,用户运行a.out时,a.out进程的有效用户身份将不再是运行a.out的用户,而是a.out文件的所有者。
s权限最常见的例子是
/usr/bin/passwd程序,它的权限位为
shell>ll /usr/bin/passwd
-r-s--x--x
1 root root 16336 Feb 13 2003 /usr/bin/passwd
说明任何一个用户运行该程序时,该程序的有效身份都将是root,这样passwd程序才有权限读取/etc/passwd文件的信息。
Linux下有四类创建子进程的函数:
system(),fork(),exec*(),popen()
原型:
#include <stdlib.h>
int system(const char *string);
system函数通过调用shell程序/bin/sh –c来执行string所指定的命令,该函数在内部是通过调用execve(“/bin/sh”,..)函数来实现的。通过system创建子进程后,原进程和子进程各自运行,相互间关联较少。如果system调用成功,将返回0。
示例:
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("Parent process id:%d/n",getpid());
pid_t iRet=fork();
if(iRet<0){
printf("Create child process fail!/n");
}else if(iRet==0){
printf("I'm child process,and id:%d ppid:%d/n",
getpid(),getppid());
}else{
printf("Create child process success,child id:%d/n",iRet);
}
return 0;
}
|
原型:
#include <unistd.h>
pid_t fork(void);
fork函数创建子进程的过程为:在进程调用fork时,系统根据现有的进程的特性几乎是完全意义的复制出一个新的子进程,复制的内容包括原进程的当时内存空间的代码、变量、对象等所有内存状态,真实和有效uid和gid,环境、资源限制、打开的文件等。通过这种复制方式创建出子进程后,原有进程和子进程都从函数fork返回,各自继续往下运行,但是原进程的fork返回值于子进程的fork返回值不同,在原进程中,fork返回子进程的pid,而在进程中,fork返回0,如果fork返回负值,表示创建子进程失败。
示例:TestFork.cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
system(“ls -l”);
return 0;
}
|
shell>g++ TestFork..cpp
shell>a.out
Parent process id:6059
I'm child process,and id:6060 ppid:6059
Create child process success,child id:6060
exec*由一组函数组成,原型:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg , ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
exec函数族的工作过程与fork完全不同,fork是在复制一份原进程,而exec函数是用exec的第一个参数指定的程序在现有进程空间中运行,并且用新进程完全覆盖掉现有进程空间。
path是可包括执行文件名的全路径名,而不是可执行文件所在的路径
file既可以是全路径名,也可以是可执行文件名
arg是可执行文件的命令行参数,可以有许多个,但通常的的一个参数应该是可执行文件的全路径名,注意最后一个参数必须为NULL。
argv是传递参数的另外一种形式,它是一个字符串的数组,一般形式为:
char *argv[]={“full path”,”param1”,”param2”,...NULL};
envp也是一个字符串数组,它指定新进程的环境变量,一般形式为:
char *envp[]={“name1=val1”,”name2=val2”,...NULL};
对有参数envp的函数调用,新进程中的全局变量environ指针指向的环境变量数组将会被envp中的内容替代。
示例:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
for(int i=0;i<argc;i++)
printf("arg%d:%s/n",i,argv[i]);
char **ppEnv=environ;
while(ppEnv && *ppEnv)
printf("%s/n",*ppEnv++);
return 0;
}
|
子进程程序GetArg.cpp,编译后的可执行文件存放到./exe/GetArg.exe
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
char *arrArg[]={"GetArg.exe","abc",NULL};
char *arrEnv[]={"env1=111","env2=222",NULL};
int iRet;
//
iRet=system("ls -l");
//iRet=execl("ls","-l",NULL);//error!
//iRet=execl("/bin/ls","ls","-l",NULL);
//iRet=execl("/bin/ls","-l",NULL);//incorrect!
//iRet=execl("./exe/GetArg.exe","GetArg.exe","abc",NULL);
//iRet=execl("./exe/GetArg.exe GetArg.exe abc",NULL);//error!
//iRet=execl("./exe/GetArg.exe","GetArg.exe abc",NULL);
//iRet=execl("./exe","GetArg.exe","abc",NULL); //error!
//iRet=execlp("./exe/GetArg.exe","GetArg.exe","abc",NULL);
//iRet=execlp("./exe","GetArg.exe","abc",NULL);//error!
//iRet=execlp("ls","ls","-l",NULL);
//iRet=execle("./exe/GetArg.exe","abc",arrEnv);//????
//iRet=execv("./exe/GetArg.exe",arrArg);
//iRet=execve("./exe/GetArg.exe",arrArg,arrEnv);
printf("iRet: %d/n",iRet);
return 0;
}
|
exec测试程序TestExec.cpp
popen函数类似于system函数,它是一种无需fork和exec就能执行外部程序的简易方法。与system的不同之处在于它使用管道工作。原型为:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
command为可执行文件的全路径和执行参数;
type可选参数为”r”或”w”,如果为”w”,则popen返回的文件流做为新进程的标准输入流,即stdin,如果为”r”,则popen返回的文件流做为新进程的标准输出流。
pclose等待新进程的结束,而不是杀新进程。
示例:
新进程源程序GetLine.cpp,可执行文件GetLine.exe
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
char szBuf[32];
fgets(szBuf,sizeof(szBuf),stdin);
fputs(szBuf,stdout);
sleep(3);
return 0;
}
|
测试程序TestPopen.cpp
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *pStream=popen("GetLine.exe","w");
if(pStream){
fputs("My god!/n",pStream);
pclose(pStream);
}else{
perror("popen fail!");
}
return 0;
}
|
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
char szBuf[32];
fgets(szBuf,sizeof(szBuf),stdin);
fputs(szBuf,stdout);
sleep(3);
return 0;
}
|
一个进程创建子进程后,如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用wait或waitpid函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
另一种情况,如果父进程先于子进程退出,则子进程成为孤儿进程,孤儿进程退出后,它的清理工作有祖先进程init自动处理。
函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait和waitpid都等待一个已经退出的子进程,并进行清理工作;
wait函数随机地等待一个已经退出的子进程,并返回该子进程的pid;
waitpid等待指定pid的子进程;
status参数是传出参数,存放子进程的退出代码;
options的可选项有2个:WNOHANG和WUNTRACED,两参数可用|合成。WNOHANG表示无论子进程是否退出都将立即返回,WUNTRACED极少使用。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void SignChildPsExit(int iSignNo)
{
pid_t pid;
int iExitCode;
pid=wait(&iExitCode);
printf("SignNo:%d child %d exit/n",iSignNo,pid);
}
int main()
{
signal(SIGCHLD,SignChildPsExit);
printf("Parent process id:%d/n",getpid());
pid_t iRet=fork();
if(iRet<0){
printf("Create child process fail!/n");
return -1;
}
}
|
进程的终止有5种方式:
l main函数的自然返回;
l 调用exit函数
l 调用_exit函数
l 调用abort函数
l 接收到能导致进程终止的信号
前3种方式为正常的终止,后2种为非正常终止。但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等资源。只是后两种终止会导致程序有些代码不会正常的执行,比如对象的析构、atexit函数的执行等。
kill
函数
原型为:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
kill函数给进程pid发送信号sig,可发送的信号可以通过shell命令kill –l查看,接收信号的进程如果不对信号进行处理,将会被自动终止。
fork以后的子进程自动继承了父进程的打开的文件,继承以后,父进程关闭调打开的文件不会对子进程造成影响。
示例:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
char szBuf[32]={0};
int iFile=open("./a.txt",O_RDONLY);
if(fork()>0){//parent process
close(iFile);
return 0;
}
//child process
sleep(3);//wait for parent process closing fd
if(read(iFile,szBuf,sizeof(szBuf)-1)<1){
perror("read fail");
}else{
printf("string:%s/n",szBuf);
}
close(iFile);
return 0;
}
用exec*创建的子进程照样可以继承已经打开的文件。
示例:
/Parent2.cpp
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
char szBuf[32]={0};
if(fork()>0)//parent process
return 0;
//child process
close(STDOUT_FILENO);
int iFile=open("./a.txt",O_WRONLY|O_CREAT,0666);
if(iFile!=STDOUT_FILENO){
perror("open a.txt fail");
return -1;
}
lseek(iFile,0,SEEK_END);
execl("./Child2.exe",NULL);
perror("execl fail!"); //must never run here!
return 0;
}
/Child2.cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello/n");
return 0;
}
如果用exec*创建子进程,虽然子进程可以自动继承父进程打开的文件,但是因为exec*后,子进程完全替换掉了父进程的空间,所以比较难获取打开的文件描述符,在这种情况下,必须采用进程间的通讯机制,让子进程知道自己继承了一些什么已经打开的文件。
示例,用FIFO传递打开的文件的信息:
///Parent3.cpp
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
int main()
{
char szBuf[32]={0};
int fdFifo,fdFile;
fdFile=open("./a.txt",O_WRONLY|O_CREAT,0666);
if(fdFile<0){
perror("open a.txt fail");
return -1;
}
lseek(fdFile,0,SEEK_END);
if(mkfifo("MyFifo",0666)<0 && errno != EEXIST){
perror("create fifo fail");
return -2;
}
if(fork()>0){//parent process
close(fdFile);
if((fdFifo=open("MyFifo",O_WRONLY))<0){
perror("open fifo fail");
return -3;
}
write(fdFifo,&fdFile,sizeof(fdFile));
close(fdFifo);
//unlink("MyFifo");
return 0;
}
//child process
execl("./Child3.exe",NULL);
perror("execl fail!"); //must never run here!
perror("execl fail!"); //must never run here!
return 0;
}
Child3.cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fdFifo,fdFile;
char szBuf[]="written by child3/n";
fdFifo=open("MyFifo",O_RDONLY);
if(fdFifo<0){
perror("Child3 open fifo fail");
return -1;
}
if(read(fdFifo,&fdFile,sizeof(fdFile))<1){
perror("read fifo fail");
return -2;
}
close(fdFifo);
write(fdFile,szBuf,strlen(szBuf));
close(fdFile);
return 0;
}