日子天天过 感受未必同---Kevin

Linux进程创建

上一篇 / 下一篇  2010-12-10 10:47:37 / 天气: 晴朗 / 心情: 平静 / 精华(3) / 置顶(3) / 不允许评论 / 个人分类:Linux

进程创建

从代码角度来看,创建一个新进程的函数声明如下:

pid_t fork(void);

其包含在 unistd.h 头文件中,其中pid_t是表示“type of process id”的32位整数, 至于函数的返回值,取决于在哪个进程中来检测该值,如果是在新创建的进程中,其为0;如果是在父进程中(创建新进程的进程),其为新创建的进程的id; 如果创建失败,则返回负值。

我们看下面的代码:

#include <stdio.h>
#include <unistd.h>

int main ()
{
    printf(
"app start...\n");
    
    pid_t id = fork();
    
    
if (id<0) {
        printf(
"error\n");
    }
else if (id==0) {
        printf(
"hi, i'm in new process, my id is %d \n", getpid());
    }
else {
        printf(
"hi, i'm in old process, the return value is %d\n", id);
    }
    
    
return 0;
}

为了方便理解,我在上面使用了getpid函数,其返回当前进程的id。

程序输出为:

app start...
hi, i'm in old process, the return value is 5429
hi, i'm in new process, my id is 5429 

另外,看到不少资料上说“fork函数是少数返回两个值的函数”,我不赞成该说法,我猜想,其之所以看上去有着不同的值,是系统创建新进程并复制父进程相关资源时,故意根据创建状态放入了不同的值。 

fork函数失败的原因主要是没有足够的资源来进行创建或者进程表满,如果是非root权限的账户,则可能被管理员设置了最大进程数。一个用户所能创建的最大进程数限制是很重要的,否则一句代码就可能把主机搞当机:for(;;) fork(); 

再看下面的代码:

#include <stdio.h>
#include <unistd.h>

int main ()
{
    printf(
"app start...\n");
    
    
int counter = 0;
    
    fork();
    
    counter++;
    
    printf(
"the counter value %d\n", counter);
    
    
return 0;
}

输出如下:

app start...
the counter value 1
the counter value 1

之所以会这样,画个图就明白了:

 

并且,新进程得到的是父进程的副本,所以,父子进程counter变量不会相互影响。 

再来一个demo:

#include <stdio.h>
#include <unistd.h>

int main ()
{
    printf(
"app start...");
    
    fork();
    
    
return 0;
}

输出为:

app start...app start...

好奇怪是吧?情况是这样的:

当你调用printf时,字符串被写入stdout缓冲区(还没刷到屏幕上的哦),然后fork,子进程复制了父进程的缓冲区,所以子进程的stdout缓冲区中也包含了“app start ...”这个字符串,然后父子进程各自运行,当他们遇到return语句时,缓冲器会被强制刷新,然后就分别将“app start...”刷到了屏幕上。如果想避免,在fork前,调用fflush强制刷新下缓冲区就可以了,在字符串后面加上“\n”也可以,因为stdout是按行缓冲的。

哈,大概就这么多,至于如何创建一个新进程以运行一个新程序,稍候我会谈exec函数,它们两者相结合就可以了~ 

运行新程序

在上一篇中我们说到,当启动一个新进程以后,新进程会复制父进程的大部份上下文并接着运行父进程中的代码,如果我们使新进程不运行原父进程的代码,转而运行另外一个程序集中的代码,这就相当于启动了一个新程序。这里的代码我们可以理解成一个可执行程序。

所以,要运行一个新程序,需要最基本的两步:

1,创建一个可运行程序的环境,也就是进程。

2,将环境中的内容替换成你所希望的,也就是用你希望运行的可执行文件去覆盖新进程中的原有映像,并从该可执行文件的起始处开始执行。

要做到第一点,非常简单,fork函数就可以(参考上一篇) ,要做到第二点,则可以利用exec函数族。

exec是一族函数的简称,包含在<unistd.h>中它们作用都一样,用一个可执行文件覆盖进程的现有映像,并转到该可执行文件的起始处开始执行。

原型如下:

int execl(const char *path, const char *arg0, ... /*, (char *)0 */);

int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);

int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *path, char *const argv[], char *const envp[]);

我们先以最简单的execl函数为例,其他的大同小异,其第一个参数path是可执行文件的路径,是绝对路径;从arg0参数开始及后面所有的是你要传递给可执行文件的命令行参数,值得注意的是,arg0是可执行文件本身(还记得C语言中老师讲main函数参数列表时所说的么),当然,不传程序本身或传一些乱七八糟的值并不代表不能通过编译或不能运行,只不过,如果可执行文件要用到arg0时会产生一些迷惑;最后有一个注释/*, (char*)0 */是提醒我们最后一个参数应该传空字符串。如何函数运行成功,则不会有任何返回值,否则返回-1,而具体的错误号会被设置在errno,errno是一个全局变量,用于程序设置错误号,跟win32的getLastError函数类似。

看下面的例子:

#include <stdio.h>
#include <unistd.h> i

nt main ()
{
printf("app start...\n");

execl("/bin/ls", "/bin/ls", "-l",NULL);

printf("app end\n");

return 0;
}

我们运行了bin目录下的ls程序,参数arg0时ls程序本身路径,arg1为-l,使得其以列表的形式列举当前目录,在我的计算机上程序输出如下:

app start...
total 12
-rw-r--r-- 1 zhouyh zhouyh 273 2010-09-06 11:09 temp.c
-rwxr-xr-x 1 zhouyh zhouyh 7175 2010-09-06 11:09 temp.exe
ls程序运行成功了。但注意到了吗?没有输出“app end”这个字符串,原因很简单,我们没有新起进程,而是直接用ls程序覆盖了main函数所在的进程。

那我们接下来,试着用fork吧,以免影响原进程。

#include <stdio.h>
#include <unistd.h>

int main ()
{
printf("app start...\n");

if(fork() == 0)
{
execl("/bin/ls", "/bin/ls", "-l", NULL);
}

printf("app end\n");

return 0;
}

我们用fork创建了一个新进程,当其成功创建后(返回值为0),我们用execl来加载ls程序并运行之。

程序的输出如下:

app start...
app end
zhouyh@ubuntu:~/Documents$ total 12
-rw-r--r-- 1 zhouyh zhouyh 229 2010-09-06 15:59 temp.c
-rwxr-xr-x 1 zhouyh zhouyh 7211 2010-09-06 16:00 temp.exe

程序的所有输出都OK了,但有一点可能和我们想象的不一样,那就是“app end”这个字符串很早就输出了而不是在最后,其实这并没有错,“app end” 是main函数所在的程序(temp.exe)即将结束时输出的,而列举文件目录的ls却完全在另外一个进程中,两个异步执行的进程,他们没有谁先谁后结束可言。

如果我们希望所有工作完成之后,即ls也执行玩以后,才输出“app end”,那么可以使用wait 以及waitpid函数,这里简单说一下wait,具体的会在“Linux进程线程学习笔记:进程控制”中讲。
pid_t wait (int * status); //包含在 <sys/wait.h> 中

wait函数讲当前进程休眠,直到该进程的某个子进程结束或者有特定的信号来唤醒。如果子进程正常结束,则讲子进程的进程id(pid)作为返回值,发生错误则返回-1,而status参数讲传出子进程的结束状态值。
针对刚才的例子,可以参考下面的代码:

#include <stdio.h> //for printf(const char)
#include <unistd.h> //for fork()
#include <sys/wait.h> //for wait(int* status)

int main ()
{
printf("app start...\n");

if(fork() == 0)
{
execl("/bin/ls", "/bin/ls", "-l", NULL);
}

int status;
wait(&status);

printf("app end\n");

return 0;
}

程序输出如下:

app start...
total 12
-rw-r--r-- 1 zhouyh zhouyh 337 2010-09-06 16:22 temp.c
-rwxr-xr-x 1 zhouyh zhouyh 7247 2010-09-06 16:22 temp.exe
app end

好了,现在回过头来看除execl外的其他几个函数 :

int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);

execlp和execl差不多,但区别在于前者会去系统环境变量查找file所指的程序的位置,所以如果通过环境变量能找到可执行文件,则file可以不是绝对路径了,比如 execlp("ls", "ls", "-l", NULL);

int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/);

与execlp不同的是,其最后一个参数作为你自定义的环境变量参数传进去,而不是查找系统环境变量

char *env[] = { "HOME=/usr/home", "LOGNAME=home",(char *)0 };

execle("/bin/ls", "ls", "-l", NULL, env);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *path, char *const argv[], char *const envp[]);

这三个函数和前面的三个类似,函数名由后缀l变成了v,其表达的含义是参数不再用参数列表传递而是用一个参数数组argv[],当然,数组最后一个元素也必须是char* 0

名字这么相近的函数,感觉好容易混淆,那么就从l,v,p,e 这样的后缀来区分吧:

l:参数为一个逗号分隔的参数列表,并以char* 0作为列表结尾

v: 参数为字符串数组,数组的最后一个元素为char* 0

p: 可以通过系统环境变量查找文件位置

e:调用者显示传入环境变量

转自:2010-09-13 作者:周银辉 来源:周银辉的blog。在此感谢原作者


TAG:

 

评分:0

我来说两句

Open Toolbar