Linux进程线程学习笔记
 

2010-09-13 作者:周银辉 来源:周银辉的blog

 

进程创建

各位同学,转换下思维,这里说的是“进程”, 不是“线程”,OK,我们开始

“进程”二字似乎总有那么些“只可意会不可言传”的韵味,维基百科是这样来解释的:

进程(英语:Process,台湾译作行程)是计算机中已运行程序的物理。进程本身不会运行,是线程的容器。程序本身只是指令的集合,进程才是程序(那些指令)的真正运行。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或不同步(平行)的方式独立运行(多线程即每一个线程都代表一个进程)。现代计算机系统可在同一段时间内加载多个程序和进程到存储器中,并借由时间共享(或称多任务),以在一个处理器上表现出同时(平行性)运行的感觉。同样的,使用多线程技术的操作系统或计算机架构,同样程序的平行进程,可在多 CPU 主机或网络上真正同时运行(在不同的 CPU 上)。进程为现今分时系统的基本运作单位。

也有朋友如此来阐述,

一个可以执行的程序;

和该进程相关联的全部数据(包括变量,内存空间,缓冲区等等);

程序的执行上下文(execution context)

我更希望将这些简化一下(或许不太准确):指令和执行指令所需的环境,指令可以理解成“代码”,环境可以理解成“上下文”

系统用一个叫做“进程表”的东西来维护中系统中的进程,进程表中的一个条目维护着存储着一个进程的相关信息,比如进程号,进程状态,寄存器值等等...

当分配给进程A的“时间片”使用完时,CPU会进行上下文切换以便运行其他进程,比如进程B,这里所谓的“上下文切换”,主要就是在操作那个“进程表”,其将进程A的相关信息(上下文)保存到其对应的进程表项中, 与之相反,其会从对应于进程B的进程表项中读取相关信息并运行之。

那么,如果进程A新建了一个进程C呢?教程表会多这样一个表项,并且该表项拥有一个唯一的ID,也就是进程号(PID),进程表项的其他值大部分与进程A的相同,具体说来,就是C和A共享代码段,并且C将A的数据空间,堆栈等复制一份 ,然后从A创建C的地方开始运行。

A和C的相似度极大,除了以下方面(来自这里: http://opengroup.org/onlinepubs/007908775/xsh/fork.html ):

The new process (child process) is an exact copy of the calling process (parent process) except as detailed below.

The child process has a unique process ID.

The child process ID also does not match any active process group ID.

The child process has a different parent process ID (that is, the process ID of the parent process).

The child process has its own copy of the parent's file descriptors. Each of the child's file descriptors refers to the same open file description with the corresponding file descriptor of the parent.

The child process has its own copy of the parent's open directory streams. Each open directory stream in the child process may share directory stream positioning with the corresponding directory stream of the parent.

The child process may have its own copy of the parent's message catalogue descriptors.

The child process' values of tms_utime, tms_stime, tms_cutime and tms_cstime are set to 0.

The time left until an alarm clock signal is reset to 0.

All semadj values are cleared.

File locks set by the parent process are not inherited by the child process.

The set of signals pending for the child process is initialised to the empty set.

Interval timers are reset in the child process.

If the Semaphores option is supported, any semaphores that are open in the parent process will also be open in the child process.

If the Process Memory Locking option is supported, the child process does not inherit any address space memory locks established by the parent process via calls to mlockall() or mlock().

Memory mappings created in the parent are retained in the child process. MAP_PRIVATE mappings inherited from the parent will also be MAP_PRIVATE mappings in the child, and any modifications to the data in these mappings made by the parent prior to calling fork()will be visible to the child. Any modifications to the data in MAP_PRIVATE mappings made by the parent after fork() returns will be visible only to the parent. Modifications to the data in MAP_PRIVATE mappings made by the child will be visible only to the child.

If the Process Scheduling option is supported, for the SCHED_FIFO and SCHED_RR scheduling policies, the child process inherits the policy and priority settings of the parent process during a fork() function. For other scheduling policies, the policy and priority settings on fork()are implementation-dependent.

If the Timers option is supported, per-process timers created by the parent are not inherited by the child process.

If the Message Passing option is supported, the child process has its own copy of the message queue descriptors of the parent. Each of the message descriptors of the child refers to the same open message queue description as the corresponding message descriptor of the parent.

If the Asynchronous Input and Output option is supported, no asynchronous input or asynchronous output operations are inherited by the child process.

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

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:调用者显示传入环境变量



基于模型的整车电子电气架构设计
嵌入式设备上的 Linux 系统开发
Linux 的并发可管理工作队列
ARM嵌入式系统的问题总结分析
嵌入式系统设计与实例开发
WinCE6.0的EBOOT概要
更多...   


UML +RoseRealtime+嵌入式
C++嵌入式系统开发
嵌入式白盒测试
手机软件测试
嵌入式软件测试
嵌入式操作系统VxWorks


中国航空 嵌入式C高质量编程
使用EA和UML进行嵌入式系统分析设计
基于SysML和EA的嵌入式系统建模
上海汽车 嵌入式软件架构设计
北京 嵌入式C高质量编程
北京 高质高效嵌入式开发
Nagra linux内核与设备驱动原理
更多...   
 
 
 
 
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号