Unix 环境编程: 进程控制

来源:互联网 发布:大数据分析工程师 编辑:程序博客网 时间:2024/05/16 00:29

进程是程序在内存中运行的一个实例,这里说出了进程与程序的一个区别就是进程是在内存空间里的,程序实际上是存储在你的硬盘上的那个文件,又可以叫做可执行文件。

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 testchmod 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 processi = 1this is in parent processi = 0</span>

this is in child processi = 1this is in parent processi = 0
输出:

-------------------------------------------------------------------------------------------

this is in child processi = 1this is in parent processi = 0

this is in child processi = 1this is in parent processi = 0
this is in child processi = 1this is in parent processi = 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, 可以配置的这个特性不错,大家可以试试看,这里不多说。


总结

这篇文章讲述了进程的创建fork exec, 进程的退出 exit, 如何避免僵尸进程waitpid,并给了简单的demo来说明fork等的应用。本文并没有讲述在什么情形中使用子进程这个特性,算是不足之处,以后尽量补上。


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


0 0
原创粉丝点击