关于进程内存布局看到最好的一篇文章 http://blog.csdn.net/DLUTBruceZhang/article/details/9080157
进程是程序在内存中运行的一个实例,这里说出了进程与程序的一个区别就是进程是在内存空间里的,程序实际上是存储在你的硬盘上的那个文件,又可以叫做可执行文件。
1. 如何标识一个进程
1)当然是用一个ID来标识一个进程啦,在Linux系统上貌似任何东西的标识都是使用一个整数也就是ID,进程当然也不例外,那么一个进程除了PID 以外还有哪些标识呢?
-->PID : 最常用的就是 process ID
-->PPID : 所有的进程都有一个父进程 Parent Process ID
-->UID : 用户ID,谁运行的这个进程
-->EUID : 有效用户ID,虽然一个进程是由用户A执行起来的,但是进程拥有的权限却可以与A不同, 这个EUID是跟可执行文件的S权限息息相关的
-->GID : 组ID
-->EGID :有效组ID
PID与PPID很直白很容易理解, UID/GID也还可以,毕竟进程都是由用户执行起来的嘛, 但是EUID?EGID是什么鬼?关于这两个ID,大家可以使用系统调用getUID() getEUID(),来观察一下,EUID与UID一般情况下是一样的,那么什么情况下会不一样呢 ?那就是当设置可执行文件的SUID这个权限的时候,也是使用了chmod u+s XXX。
SUID又是什么鬼? OK,看下面的实验吧
首先写一个测试程序
<span style="font-family:Courier New;"><span style="font-family:Courier New;">#include <stdio.h>
#include <unistd.h>
int main()
{
uid_t uid = getuid();
uid_t euid = geteuid();
printf("uid = %d, euid = %d\n", uid, euid);
return 0;
}</span></span>
gcc test.c -o test 之后得到 可执行文件 : test
<span style="font-family:Courier New;"><span style="font-family:Courier New;">ls -l
-rwxrwxr-x 1 gengj gengj 13451 8月 22 10:25 test
<span style="color:#ff0000;">chown root test
chmod u+x test</span>
ls -l
-rwsrwxr-x 1 root gengj 13451 8月 22 10:25 test
</span></span>
看到没有, 刚开始 test 文件的 用户和组都是gengj, 这个时候执行test,得到 uid == euid,但是当执行了后边的操作也就是设置了S权限之后(这个时候实际上test是以root权限来运行的,也就是它的有效用户是 root),再执行,可以看到euid变成了 0, UID没有改变。关于GID/EGID与上面的描述是一样的。
2)现在我们知道了如何获得关于进程的各种ID,那么怎么对这些ID进程设置呢?
实际上PPID是没有办法改变的,PID是内核分配的,这两个ID是不能改变的,也没有必要改变。可以改变的是UID EUID GID EGID, 这里我们不讨论组只讨论用户 ID, 因为他们的操作是一样的。
设置UID我们可以使用系统调用 setuid(uid_t uid); 关于这个函数需要说明一下:
-->如果进程拥有root权限的话,该进程调用setuid(uid) 可以把 本进程的UID, EUID, saved set-uid全部设置成参数 uid;
-->如果进程不是root权限,参数uid==UID 或者 uid == SUID的话, 本函数将会把EUID设置成uid,其他的ID都不变。
各种ID搞得人比较晕,我也不是特别的清楚, 不过只要记得不同的UID会使得进程拥有不同的权限,如果设置了EUID, 则表示了进程的实际可以拥有的权限。
2. 创建新的进程 fork
1)系统调用 pid_t fork(void) 是用来创建一个新的子进程的, 这个函数比较特殊,一次调用会返回两次:
<span style="font-family:Courier New;">#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
int i = 0;
if ((pid = fork()) < 0) {
printf("fork failed\n");
return -1;
} else if (pid == 0) { // this is child process
printf("this is in child process\n");
i++;
} else { //this is in parent process
sleep(2);
printf("this is in parent process\n");
}
//this is in bothprocess
printf("i = %d\n", i);
return 0;
}</span>
<span style="font-family:Courier New;"><pre style="-webkit-user-select: text; position: absolute; top: -99px;">this is in child process
i = 1
this is in parent process
i = 0</span>
this is in child process
i = 1
this is in parent process
i = 0
输出:
-------------------------------------------------------------------------------------------
this is in child process
i = 1
this is in parent process
i = 0
this is in child process
i = 1
this is in parent process
i = 0
this is in child process
i = 1
this is in parent process
i = 0
this is in child process
i = 1
...(sleep 2)
this is in parent process
i = 0
-------------------------------------------------------------------------------------------
如上显示的, fork返回的pid如果为0 则表示这是从子进程返回的值,如果大于 0则表示是从父进程返回的值,返回值就是子进程的PID。
如上显示的,fork之后,父子进程都会继续执行接下来的代码,也就是说父子进程共享一个代码段 (text segment),但是数据是不一样的,因为在子进程对 i 赋值并不会影响父进程中的 i,说明,父子进程的数据段是不一样的,实际上子进程复制了父进程的数据段,所以导致父进程与子进程都有一个数据 i 的copy,各自的改变并不会影响另一个进程。
2) fork中的文件描述符
父进程中打开的文件描述符将在子进程中同样有效,也就是说父子进程共享文件描述符。这种共享会导致父子进程产生竞争现象,需要在编程中避免这种竞争。
3)子进程继承的东东与没有继承的东东
先看看有哪些东西没有被子进程继承吧:
-- PID & PPID
-- time (tms_utime, tms_stime) 在子进程中被置 0
-- 文件锁 在子进程中没有
-- 定时器
-- 信号集
被继承下来的东西:
-- UID , GID
-- 进程组
-- 会话(session) ID
-- 控制终端
-- set-user-id/ set-group-id
-- root directory
-- current work directory
-- 文件权限掩码 mask
-- 信号掩码
-- close-on-exec 标志
-- 环境变量
-- 共享内存
-- 内存映射
4) fork为什么会失败
fork失败意味着系统不能生成新的进程啦,这有可能是由于系统资源不足造成的, 也有可能是一个real user id 的进程数超出了限制造成的。
5)fork 与 exec函数
fork会产生一个新的进程,子进程会有父进程地址空间的一份copy, 但是如果在fork之后的子进程中调用exec函数,那么子进程将会被新的程序替代,子进程将会从新程序的main函数开始执行。
这里替换的意思不是创建一个新的进程,而是继续在原来的地址空间里面继续执行,只不过代码段,数据段,堆栈全部被替换啦,而且当新程序结束后也不再返回到子进程中啦。既然是在原子进程空间里面运行新的程序,PID当然还是不会改变的啦。
<span style="font-family:Courier New;">#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
if ((pid = fork()) < 0) {
printf("fork failed\n");
return -1;
} else if (pid == 0) { // this is child process
printf("this is in child process\n");
printf("exec a new program\n");</span>
<span style="font-family:Courier New;"> execlp("ls", "ls", "-1", NULL); <span style="color:#ff0000;">// run another program</span>
printf(" I am back into child process\n"); <span style="color:#ff0000;">// never come back to here</span>
} else {
sleep(2);
printf("this is in parent process\n");
global_num++;
}
//this is in bothprocess
printf("i = %d, global_num = %d\n", i, global_num);
return 0;
}</span>
讲到了exec 函数,有必要提一下另外一个系统调用system(), 实际上system()函数是用exec函数来实现的,只不过多了一些错误检查。这个函数用于在我们的程序中执行另外的可执行文件,直到结束,也就是说它是阻塞的,一个简单的system实现如下.
parent-process --> fork --> exec
|-----------------------------waitpid()
3 进程结束
结束一个进程有很多种方法:
正常的退出
-- main函数中调用 return 或者 exit(),这两者是等价的
-- 调用 _exit 或者 _Exit
不正常退出
-- 调用abort, 将会产生SIGABRT信号
-- 接收到特定的信号, 比如运行中我们按下ctl-c也就是发送了TERM 信号给进程
不正常的退出现在先不讲,只讨论一下正常的退出中的exit _exit _Exit
1) exit 与return一样是一种比较安全温和的退出方式,将会flush 所有IO然后关闭所有的文件, 依次调用注册的at_exit 函数, 最后退出
2)_exit 与_Exit这是一种简单粗暴的退出方式,不会调用at_exit注册的函数,也可能不会flush IO (it depends on OS implementation)。
当子进程正常退出时,父进程可以获得子进程的退出状态值,退出状态值是exit函数的参数,当子进程非正常退出时,内核来把退出状态值送给父进程,总之,父进程总是可以获得子进程的退出状态值。 这里我们考虑以下两种情况
1)父进程先于子进程退出
如果父进程比子进程先退出,那么init将会成为子进程的父进程,这是因为Linux要确保每一个进程都有一个父进程
2)子进程先于父进程退出
如果子进程先退出啦,那么内核会在内存中维护这个子进程的一些信息,以便父进程在调用wait或者waitpid时能够获得子进程的退出状态,一般来讲内核保存的信息必然会包括PID和退出状态值, 因为这些都是wait函数需要的。
子进程退出后,如果父进程调用了wait或者waitpid,那么内核就不需要在保存这些残留信息了,那么这个子进程就完全退出了。如果父进程不调用wait&waitpid,那么这个子进程就变成了僵尸进程(zombie)。
是时候说说wait & waitpid函数了,这两个函数都会使得内核对子进程进行清理,也就是消除zombie。 我们先来讲讲这两个函数的特点吧
1)wait: 阻塞的,直到有一个子进程退出他才返回,实际上任何一个子进程的退出都会导致wait返回,也就是说,如果有很多子进程就需要调用很多次wait;如果没有子进程的话,wait也会立即返回,只不过是返回error。
2) waitpid: 相对于wait,waitpid比较人性化,他会等待特定的有其参数指定的进程退出。他可以是阻塞的也可以通过其参数配置成非阻塞的。haha, 可以配置的这个特性不错,大家可以试试看,这里不多说。
4 进程的优先级调整
优先级越高,越有可能被优先调度使用CPU,在Linux系统中可以使用nice()函数来更改本进程的优先级,
int nice(int incr);
优先级的有小范围是0~(2*NZERO-1),NZERO 表示默认的nice值。 值越小优先级越高。参数incr表示要增加的值而非绝对值。
此外还有连个函数可以用来获得和设置进程(组)的优先级:
int getpriority(int which, id_t who); which的有效值: PRIO_PROCESS, PRIO_PGRP, PRIO_USER.
int setpriority(int which, id_t who, int value), 注意value被增加到NZERO上作为新的nice值。
5 进程的时间
我们知道,可以用命令time 来获得一个程序运行的时间, 例如:
real0m0.003s
user 0m0.002s
sys 0m0.000s
这就是用times()系统调用来获取的, 函数原型如下:
clock_t times(struct tms *buf); //获得的时间是从之前的某一时刻开始的时间,没有什么意义,必须调用两次times然后取相对值。
上面的real time 是返回值相减再除以sysconf(_SC_CLK_TCK)的值
user 和 sys time 是结构体struct tms 中成员相减再除以sysconf(_SC_CLK_TCK)的值。
大家可以写一个自己的time命令哟。
总结
这篇文章讲述了进程的创建fork exec, 进程的退出 exit, 如何避免僵尸进程waitpid,并给了简单的demo来说明fork等的应用。本文并没有讲述在什么情形中使用子进程这个特性,算是不足之处,以后尽量补上。
欢迎阅读提问,如果有不对的地方,请指正。