Unix 环境编程: 进程控制

关于进程内存布局看到最好的一篇文章 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等的应用。本文并没有讲述在什么情形中使用子进程这个特性,算是不足之处,以后尽量补上。


欢迎阅读提问,如果有不对的地方,请指正。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值