进程控制(2): 进程操作

进程是系统中基本的执行单位,本节将介绍基本的进程控制原语,包括进程的创建与退出,以及设置除进程标识符(PID)以外的其他标识符。


1 创建进程

Linux系统允许任何一个用户进程创建一个子进程,创建成功后,子进程存在于系统之中,并且独立于父进程。该子进程可以接受系统调度,可以得到分配的系统资源。系统也可以检测到子进程的存在,并且赋予它与父进程同样的权利。

Linux系统下使用fork()函数创建一个子进程,其函数原型如下:

#include <unistd.h>
pid_t fork(void);

在讨论fork()函数之前,有必要先明确父进程和子进程两个概念。除了0号进程(init进程,该进程是系统自举时由系统创建的)以外,Linux系统中的任何一个进程都是由其他进程创建的。创建新进程的进程,即调用fork()函数的进程就是父进程,而新创建的进程就是子进程。

fork()函数不需要参数,返回值是一个进程标识符(PID)。对于返回值,有以下3种情况:

(1) 对于父进程,fork()函数返回新创建的子进程的ID。

(2) 对于子进程,fork()函数返回0。由于系统的0号进程是内核进程,所以子进程的进程标识符不会是0,由此可以用来区别父进程和子进程。

(3) 如果创建出错,则fork()函数返回-1。

fork()函数会创建一个新的进程,并从内核中为此进程分配一个新的可用的进程标识符(PID),之后,为这个新进程分配进程空间,并将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,系统中又多了一个进程,这个进程和父进程一模一样,两个进程都要接受系统的调度。

注意:由于在复制时复制了父进程的堆栈段,所以两个进程都停留在了fork()函数中,等待返回。因此,fork()函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。

下面给出的示例程序用来创建一个子进程,该程序在父进程和子进程中分别输出不同的内容。

//@file fork.c
//@brief create a new process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    pid_t pid;//to store pid value

    pid = fork();//create a new process
    if (pid < 0)
    {
        //error
        perror("fail to fork");
        exit(-1);
    }
    else if (pid == 0)
    {
        //sub-process
        printf("Sub-process, PID: %u, PPID: %u\n", getpid(), getppid());
    }
    else
    {
        //parent process
        printf("Parent, PID: %u, Sub-process PID: %u\n", getpid(), pid);
    }

    return 0;
}

程序运行结果如下:

[email protected]:~/Documents/c_code$ ./fork
Parent, PID: 2598, Sub-process PID: 2599
Sub-process, PID: 2599, PPID: 2598

由于创建的新进程和父进程在系统看来是地位平等的两个进程,所以运行机会也是一样的,我们不能够对其执行先后顺序进行假设,先执行哪一个进程取决于系统的调度算法。如果想要指定运行的顺序,则需要执行额外的操作。正因为如此,程序在运行时并不能保证输出顺序和上面所描述的一致。


2 父子进程的共享资源

子进程完全复制了父进程的地址空间的内容,包括堆栈段和数据段的内容。子进程并没有复制代码段,而是和父进程共用代码段。这样做是存在其合理依据的,因为子进程可能执行不同的流程,那么就会改变数据段和堆栈段,因此需要分开存储父子进程各自的数据段和堆栈段。但是代码段是只读的,不存在被修改的问题,因此这一个段可以让父子进程共享,以节省存储空间,如下图所示。

下面给出一个示例来说明这个问题。该程序定义了一个全局变量global、一个局部变量stack和一个指针heap。该指针用来指向一块动态分配的内存区域。之后,该程序创建一个子进程,在子进程中修改global、stack和动态分配的内存中变量的值。然后在父子进程中分别打印出这些变量的值。由于父子进程的运行顺序是不确定的,因此我们先让父进程额外休眠2秒,以保证子进程先运行。

//@file fork.c
//@brief resource sharing between parent-process and sub-process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int global = 1; /*global variable, stored at data section*/

int main(void)
{
    pid_t pid;//to store pid value
    int   stack = 1;//local variable, stored at stack
    int  *heap;//pointer to a heap variable

    heap = (int *)malloc(sizeof(int));
    *heap = 2;//set the heap value to 2

    pid = fork();//create a new process
    if (pid < 0)
    {
        //error
        perror("fail to fork");
        exit(-1);
    }
    else if (pid == 0)
    {
        //sub-process, change values
        global++;
        stack++;
        (*heap)++;
        //print all values
        printf("In sub-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);
        exit(0);
    }
    else
    {
        //parent process
        sleep(2);//sleep 2 secends to make sure the sub-process runs first
        printf("In parent-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);
    }

    return 0;
}

程序运行效果如下:

[email protected]:~/Documents/c_code$ ./fork
In sub-process, global: 2, stack: 2, heap: 3
In parent-process, global: 1, stack: 1, heap: 2

由于父进程休眠了2秒钟,子进程先于父进程运行,因此会先在子进程中修改数据段和堆栈段中的内容。因此不难看出,子进程对这些数据段和堆栈段中内容的修改并不会影响到父进程的进程环境。

父进程的资源大部分被fork()函数所复制,只有小部分是子进程与父进程不同的。子进程继承的资源情况如下表所示:

现在的Linux内核实现fork()函数时往往实现了在创建子进程时并不立即复制父进程的数据段和堆栈段,而是当子进程修改这些数据内容时复制才会发生,内核才会给子进程分配进程空间,将父进程的内容复制过来,然后继续后面的操作。这样的实现更加合理,对于一些只是为了复制自身完成一些工作的进程来说,这样做的效率会更高。这也是现代操作系统中一个重要的概念——“写时复制”的一个重要体现。


3 fork出错的情况

有两种情况可能会导致fork()函数出错:

(1) 系统中已经有太多的进程存在了

(2) 调用fork()函数的用户进程太多了

一般情况下,系统都会对一个用户所创建的进程数加以限制。如果操作系统不对其加限制,那么恶意用户可以利用这一缺陷攻击系统。下面是一个利用进程的特性编写的一个病毒程序,该程序是一个死循环,在循环中不断调用fork()函数来创建子进程,直到系统中不能容纳如此多的进程而崩溃为止。下图展示了这种情况:

//@file fork.c
//@brief do bad thing, always create sub-process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    while (1) fork();
    return 0;
}

程序运行结果如下:

[email protected]:~/Documents/c_code$ ./fork &
[1] 13618
[email protected]-machine:~/Documents/c_code$ ps -u xiaomanon
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable

系统可能会变得很慢,以上是本人在Ubuntu 14.04LTS(虚拟机)上的测试结果,需要重启才能解决问题。

注意:在现在的操作系统中,这种情况是不被允许的。因此,系统中限制了一个用户创建的进程的数量,这种进攻已经不能奏效。


4 创建共享空间的子进程

进程在创建一个新的子进程之后,子进程的地址空间完全和父进程分开。父子进程是两个独立的进程,接受系统调度和分配系统资源的机会均等,因此父进程和子进程更像是一对兄弟。如果父子进程共用父进程的地址空间,则子进程就不是独立于父进程的。

Linux环境下提供了一个与fork()函数类似的函数,也可以用来创建一个子进程,只不过新进程与父进程共用父进程的地址空间,其函数原型如下:

#include <unistd.h>
pid_t vfork(void);

vfork()和fork()函数的区别有以下两点:

(1) vfork()函数产生的子进程和父进程完全共享地址空间,包括代码段、数据段和堆栈段,子进程对这些共享资源所做的修改,可以影响到父进程。由此可知,vfork()函数与其说是产生了一个进程,还不如说是产生了一个线程。

(2) vfork()函数产生的子进程一定比父进程先运行,也就是说父进程调用了vfork()函数后会等待子进程运行后再运行。

下面的示例程序用来验证以上两点。在子进程中,我们先让其休眠2秒以释放CPU控制权,在前面的fork()示例代码中我们已经知道这样会导致其他线程先运行,也就是说如果休眠后父进程先运行的话,则第(2)点则为假;否则为真。第(2)点为真,则会先执行子进程,那么全局变量便会被修改,如果第(1)点为真,那么后执行的父进程也会输出与子进程相同的内容。代码如下:

//@file vfork.c
//@brief vfork() usage
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int global = 1;

int main(void)
{
    pid_t pid;
    int   stack = 1;
    int  *heap;

    heap = (int *)malloc(sizeof(int));
    *heap = 1;

    pid = vfork();
    if (pid < 0)
    {
        perror("fail to vfork");
        exit(-1);
    }
    else if (pid == 0)
    {
        //sub-process, change values
        sleep(2);//release cpu controlling
        global = 999;
        stack  = 888;
        *heap  = 777;
        //print all values
        printf("In sub-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);
        exit(0);
    }
    else
    {
        //parent-process
        printf("In parent-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);
    }

    return 0;
}

程序运行效果如下:

[email protected]:~/Documents/c_code$ ./vfork 
In sub-process, global: 999, stack: 888, heap: 777
In parent-process, global: 999, stack: 888, heap: 777

注意:如果不在子进程中添加exit()函数退出的话,会导致执行父进程时出现段错误,原因目前还没弄明白。


5 在函数内部调用vfork

在使用vfork()函数时应该注意不要在任何函数中调用vfork()函数。下面的示例是在一个非main函数中调用了vfork()函数。该程序定义了一个函数f1(),该函数内部调用了vfork()函数。之后,又定义了一个函数f2(),这个函数没有实际的意义,只是用来覆盖函数f1()调用时的栈帧。main函数中先调用f1()函数,接着调用f2()函数。

//@file vfork.c
//@brief vfork() usage
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int f1(void)
{
    vfork();
    return 0;
}

int f2(int a, int b)
{
    return a+b;
}

int main(void)
{
    int c;

    f1();
    c = f2(1,2);
    printf("%d\n",c);

    return 0;
}

程序运行效果如下:

[email protected]:~/Documents/c_code$ ./vfork
3
Segmentation fault (core dumped)

通过上面的程序运行结果可以看出,一个进程运行正常,打印出了预期结果,而另一个进程似乎出了问题,发生了段错误。出现这种情况的原因可以用下图来分析一下:

左边这张图说明调用vfork()之后产生了一个子进程,并且和父进程共享堆栈段,两个进程都要从f1()函数返回。由于子进程先于父进程运行,所以子进程先从f1()函数中返回,并且调用f2()函数,其栈帧覆盖了原来f1()函数的栈帧。当子进程运行结束,父进程开始运行时,就出现了右图的情景,父进程需要从f1()函数返回,但是f1()函数的栈帧已经被f2()函数的所替代,因此就会出现父进程返回出错,发生段错误的情况。

由此可知,使用vfork()函数之后,子进程对父进程的影响是巨大的,其同步措施势在必行。



6 退出进程

当一个进程需要退出时,需要调用退出函数。Linux环境下使用exit()函数退出进程,其函数原型如下:

#include <stdlib.h>
void exit(int status);

exit()函数的参数表示进程的退出状态,这个状态的值是一个整型,保存在全局变量$?中,在shell中可以通过“echo $?”来检查退出状态值。

注意:这个退出函数会深入内核注销掉进程的内核数据结构,并且释放掉进程的资源。


7 exit函数与内核函数的关系

exit函数是一个标准的库函数,其内部封装了Linux系统调用_exit()函数。两者的主要区别在于exit()函数会在用户空间做一些善后工作,例如清理用户的I/O缓冲区,将其内容写入 磁盘文件等,之后才进入内核释放用户进程的地址空间;而_exit()函数直接进入内核释放用户进程的地址空间,所有用户空间的缓冲区内容都将丢失。


8 设置进程所有者

每个进程都有两个用户ID,实际用户ID和有效用户ID。通常这两个ID的值是相等的,其取值为进程所有者的用户ID。但是,在有些场合需要改变进程的有效用户ID。Linux环境下使用setuid()函数改变一个进程的实际用户ID和有效用户ID,其函数原型如下:

#include <unistd.h>
int setuid(uid_t uid);

setuid()函数的参数表示改变后的新用户ID,如果成功修改当前进程的实际用户ID和有效用户ID,函数返回值为0;如果失败,则返回-1。只有两种用户可以修改进程的实际用户ID和有效用户ID:

(1) 根用户:根用户可以将进程的实际用户ID和有效用户ID更换。

(2) 其他用户:其该用户的用户ID等于进程的实际用户ID或者保存的用户ID。

也就是说,用户可以将自己的有效用户ID改回去。这种情况多出现于下面的情况:一个进程需要具有某种权限,所以将其有效用户ID设置为具有这种权限的用户ID,当进程不需要这种权限时,进程还原自己之前的有效用户ID,使自己的权限复原。下面给出一个修改的示例:

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

int main(void)
{
    uid_t uid, euid;

    uid = getuid();
    euid = geteuid();
    printf("Before, uid: %d, euid: %d\n", uid, euid);

    if (setuid(1024) == -1)
    {
        perror("fail to set uid");
        exit(-1);
    }

    uid = getuid();
    euid = geteuid();
    printf("After, uid: %d, euid: %d\n", uid, euid);

    return 0;
}

程序运行效果如下:

[email protected]:~/Documents/c_code$ ./setuid
Before, uid: 1000, euid: 1000
fail to set uid: Operation not permitted
[email protected]-machine:~/Documents/c_code$ sudo ./setuid 
Before, uid: 0, euid: 0
After, uid: 1024, euid: 1024

说明:为了保证程序正确运行,用户应当具有该用户权限。以上示例中,当前用户就没有修改uid的权限,而使用超级用户权限时,能够成功修改。那么,如何让当前用户拥有修改用户ID的权限呢?

Linux环境下还提供了只修改有效用户ID的函数seteuid(),以及修改修改实际组ID和有效组ID的函数,其参数和返回值含义与setuid()的类似,函数原型如下所示:

#include <unistd.h>
int seteuid(uid_t uid);
int setgid(gid_t gid);
int setegid(gid_t gid);

9 参考文献

[1] 吴岳,Linux C程序设计大全,清华大学出版社

时间: 2024-10-13 05:41:46

进程控制(2): 进程操作的相关文章

(转)进程控制:进程的创建、终止、阻塞、唤醒和切换

进程控制:进程的创建.终止.阻塞.唤醒和切换 进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程.撤销已有进程.实现进程状态转换等功能.在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位. 进程的创建 允许一个进程创建另一个进程.此时创建者称为父进程,被创建的进程称为子进程.子进程可以继承父进程所拥有的资源.当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程.此外,在撤销父进程时,也必须同时撤销其所有的子进程.

进程控制在进程管理中的作用

 进程控制是进程管理中最基本的功能.它用于创建一个新进程,终止一个已完成的进程,或者去终止一个因出现某事件而使其无法运行下去的进程,还可负责进程运行中的状态转换. 一.创建进程 1.引起创建进程的事件 在多道程序环境中,只有(作为)进程(时)才能在系统中运行.因此,为使程序能运行,就必须为它创建进程.导致一个进程去创建另一个进程的典型事件,可以有以下四类: 1) 用户登录 在分时系统中,用户在终端键入登录命令后,如果是合法用户,系统将为该终端建立一个进程,并把它插入到就绪队列中. 2)作业调

进程控制(二)---进程标识符

Linux操作系统中为了区分每一个进程,为每个进程分配一个唯一的进程号,也称为进程ID.进程 ID 是保存在进程的 PCB 中,属于进程的内核资源. 每个进程的进程 ID 虽然是唯一的,但是进程 ID 是可以重用的,当一个进程被终止时,其所有的资源将会被释放,也包括进程 ID.当系统再次创建一个新的进程时,可以为其分配已经被释放的进程 ID .由于操作系统采用延迟重用算法,所以为刚创建的进程分配的进程 ID通常不会是刚被释放的进程 ID . 操作系统中有些进程的进程 ID 是固定的,比如 调度进

进程控制(Note for apue and csapp)

1. Introduction We now turn to the process control provided by the UNIX System. This includes the creation of new processes, program execution, and process termination. We also look at the various IDs that are the property of the process - real, effe

进程控制概念简介 多线程上篇(三)

进程控制 进程的基本数据信息是操作系统控制管理进程的数据集合,这些信息就是用来控制进程的,此处我们说的进程控制就是进程的管理. 比如进程有状态,那么进程的创建.终止,状态的切换,这都不是进程自主进行的,都是通过操作系统进行管理的 如下图所示,所有的相关数据都是操作系统用来管理维护进程的 操作系统抽象出进程概念的核心是为了运行程序 所以进程的执行态是最为核心的 其他的状态则是为了更好的控制管理进程以及进程的并发执行而附加的 所以,一定程度上来讲,操作系统对于进程的控制,可以认为是对于进程的不同状态

进程控制(二)与linux下的自有服务

一.进程动态信息查看top 第一部分 统计信息 [[email protected] ~]# top top - 19:22:52 up 1:32, 2 users, load average: 0.00, 0.00, 0.00 Tasks: 106 total, 1 running, 105 sleeping, 0 stopped, 0 zombie Cpu(s): 0.1%us, 0.1%sy, 0.0%ni, 99.6%id, 0.0%wa, 0.0%hi, 0.2%si, 0.0%st

Linux进程控制程序设计

一.进程控制理论基础 进程:是一个具有一定独立功能的程序的一次运行活动.程序是静态的,程序在运行的时候是进程. 1.进程的特点: 动态性:区别于程序的显著特性 并发性:多个进程可以同时执行 独立性:独立的 异步性:进程与进程之间可以进行异步操作 2.进程三态: 进程的ID(PID):标志进程的唯一数字. 父进程ID(PPID) 启动进程的用户ID(UID) 3.进程互斥 进程互斥是指当有若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者

小何讲进程: Linux进程控制编程 (fork、vfork)

所谓进程控制,就是系统使用一些具有特定功能的程序段来创建进程.撤消进程以及完成进程在各种状态之间的转换, 从而达到多进程高效率并发执行和协调资源共享的目的.进程控制是进程管理和处理机管理的一个重要任务. 1. fork()创建进程 在Linux系统中,除了系统启动之后的第一个进程(根进程)由系统来创建外, 其余所有进程都必须由已存在的进程来创建新创建的进程叫子进程,而创建子进程的进程叫父进程, 具有相同父进程的进程叫兄弟进程. 在Linux中创建一个新进程的方法是使用fork()函数. fork

[APUE]进程控制(上)

一.进程标识 进程ID 0是调度进程,常常被称为交换进程(swapper).该进程并不执行任何磁盘上的程序--它是内核的一部分,因此也被称为系统进程.进程ID 1是init进程,在自举(bootstrapping)过程结束时由内核调用.该进程的程序文件在UNIX的早期版本中是/etc/init,在较新版本中是/sbin/init.此进程负责在内核自举后启动一个UNIX系统.init通常读与系统有关的初始化(/etc/rc*文件),并将系统引导到一个状态(例如多用户).init进程决不会终止.它是