前言:

关于进程控制这块的好文很多,下面转载的这篇内容很丰富,也会举适当的栗子,与其写的一知半解不如参读学习别人的优秀博文,感谢原作,本系列摘录自:https://www.cnblogs.com/xiaomanon/p/4195327.html。如果有不妥之处,请告知,会删除的,再次感谢o(*^▽^*)┛


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

进程(英语:process),是计算机中已运行程序的实体。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时(平行性)运行的感觉。同样的,使用多线程技术(多线程即每一个线程都代表一个进程内的一个独立执行上下文)的操作系统或计算机架构,同样程序的平行线程,可在多CPU主机或网络上真正同时运行(在不同的CPU上)。

注:以上内容来自维基百科。

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


1 创建进程

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

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

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

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

补充(维基百科):

在UNIX里,除了进程0(即PID=0的交换进程,Swapper Process)以外的所有进程都是由其他进程使用系统调用fork创建的,这里调用fork创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程。操作系统内核以进程标识符(Process Identifier,即PID)来识别进程。进程0是系统引导时创建的一个特殊进程,在其调用fork创建出一个子进程(即PID=1的进程1,又称init)后,进程0就转为交换进程(有时也被称为空闲进程),而进程1(init进程)就是系统里其他所有进程的祖先。

进程0:Linux引导中创建的第一个进程,完成加载系统后,演变为进程调度、交换及存储管理进程。

进程1:init 进程,由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程。

Linux中1号进程是由0号进程来创建的,因此必须要知道的是如何创建0号进程,由于在创建进程时,程序一直运行在内核态,而进程运行在用户态,因此创建0号进程涉及到特权级的变化,即从特权级0变到特权级3,Linux是通过模拟中断返回来实现特权级的变化以及创建0号进程,通过将0号进程的代码段选择子以及程序计数器EIP直接压入内核态堆栈,然后利用iret汇编指令中断返回跳转到0号进程运行。

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 valuepid = fork();//create a new processif (pid < 0){//errorperror("fail to fork");exit(-1);}else if (pid == 0){//sub-processprintf("Sub-process, PID: %u, PPID: %u\n", getpid(), getppid());}else{//parent processprintf("Parent, PID: %u, Sub-process PID: %u\n", getpid(), pid);}return 0;
}

程序运行结果如下:

xiaomanon@xiaomanon-machine:~/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 valueint   stack = 1;//local variable, stored at stackint  *heap;//pointer to a heap variableheap = (int *)malloc(sizeof(int));*heap = 2;//set the heap value to 2pid = fork();//create a new processif (pid < 0){//errorperror("fail to fork");exit(-1);}else if (pid == 0){//sub-process, change valuesglobal++;stack++;(*heap)++;//print all valuesprintf("In sub-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);exit(0);}else{//parent processsleep(2);//sleep 2 secends to make sure the sub-process runs firstprintf("In parent-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);}return 0;
}

程序运行效果如下:

xiaomanon@xiaomanon-machine:~/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;
}

程序运行结果如下:

xiaomanon@xiaomanon-machine:~/Documents/c_code$ ./fork &
[1] 13618
xiaomanon@xiaomanon-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 valuessleep(2);//release cpu controllingglobal = 999;stack  = 888;*heap  = 777;//print all valuesprintf("In sub-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);exit(0);}else{//parent-processprintf("In parent-process, global: %d, stack: %d, heap: %d\n", global, stack, *heap);}return 0;
}

程序运行效果如下:

xiaomanon@xiaomanon-machine:~/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;
}

程序运行效果如下:

xiaomanon@xiaomanon-machine:~/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;
}

程序运行效果如下:

xiaomanon@xiaomanon-machine:~/Documents/c_code$ ./setuid
Before, uid: 1000, euid: 1000
fail to set uid: Operation not permitted
xiaomanon@xiaomanon-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程序设计大全,清华大学出版社

[2] IBM, UNIX进程揭秘, developerWorks

进程控制(2):进程操作相关推荐

  1. 广州大学学生实验报告,进程控制与进程通信

                                                    广州大学学生实验报告 开课学院及实验室: 计算机科学与网络工程学院  电子楼418B        20 ...

  2. Linux下的C编程实战(开发平台搭建,文件系统编程,进程控制与进程通信编程,“线程”控制与“线程”通信编程,驱动程序设计,专家问答)

    Linux下的C编程实战(一) ――开发平台搭建 1.引言 Linux操作系统在服务器领域的应用和普及已经有较长的历史,这源于它的开源特点以及其超越Windows的安全性和稳定性.而近年来,Linux ...

  3. c语言进程控制实验报告,操作系统进程的创建与控制实验报告.doc

    操作系统实验报告 实验一 进程的创建和控制 班 级: 12计算机12班 学 号: 127401219 姓 名: 刘艳仙 成 绩: 2013年 实验目的 1.掌握进程的概念,明确进程的含义 2.复习C语 ...

  4. 进程控制:进程的创建、终止、阻塞、唤醒和切换

    进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程.撤销已有进程.实现进程状态转换等功能.在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不 ...

  5. Linux_进程控制(创建进程,等待进程,进程终止)

    文章目录 1.创建进程 1.1 fork()函数初识 1.2 fork()创建进程代码示例 2.等待进程 2.1 进程等待概念 2.2进程等待必要性 2.3 进程等待方法 2.3.1 wait 2.3 ...

  6. 【Linux】进程控制(进程创建、进程终止、进程等待、进程替换)

    文章目录 一.进程创建 1.1 系统调用 fork 1.2 理解 fork 的返回值 1.3 写时拷贝策略 二.进程终止 2.1 main 函数的返回值 2.2 进程退出的几种情况 2.3 进程退出码 ...

  7. 684-进程进程控制多进程进程控制块

    什么是进程? 进程是由程序变换而来,介绍进程时,需要先从程序说起. (1)当程序没有运行时 当程序没有被运行时,该程序(可执行文件)只是存放在硬盘上的静态数据,与你写的word文件中的数据没有本质区别 ...

  8. 进程控制(进程创建与终止 | 进程等待 | 程序替换)

    文章目录 一.进程创建 1. fork函数 2. fork创建进程 3. 写时拷贝 二.进程终止 1. 进程退出有三种情况 2. 常见进程终止方法 三.进程等待 背景(必要性) 1. 进程等待的方法 ...

  9. Linux系统编程之进程控制(进程创建,fork函数,进程中止,进程等待,程序替换)

    进程创建 fork()------复制,返回值,写时复制 vfork()创建子进程-子进程与父进程共用同一块虚拟地址空间, 为了防止调用栈混乱,因此阻塞父进程直到子进程调用exit()退出或者进行程序 ...

  10. 【Linux】linux进程--进程控制:进程创建、进程终止、进程等待、进程程序替换

    目录 1.进程创建 1)重温fork():让正在运行的进程创建出来一个子进程:从已存在的进程中创建一个新的进程,新进程为子进程而远进程为父进程. 2)fork内部完成的事情 3)用户空间 & ...

最新文章

  1. 修改属性使按钮处于无验证状态
  2. 苹果手机做文件服务器,iOS企业账号打包发布App到自己服务器上
  3. python time.time()计算代码运行时间
  4. Android Scroller完全解析,关于Scroller你所需知道的一切
  5. 业界真的需要水下数据中心?微软的确认为如此
  6. AssetManager (资源路径+当前手机配置信息)
  7. 半年内使用两次借呗就没法申请房贷?
  8. 懒办法1篇文10分钟快速入门MySQL增删查改
  9. 华为P50系列终于要来了!但最大问题却是...
  10. 【原】无脑操作:ElasticSearch学习笔记(01)
  11. syslog(LOG_ERR, Error: errcode=%d, message=%s, errcode, errmsg);
  12. scala的静态属性和静态方法
  13. 洛谷 P1168 中位数 堆
  14. 简单python程序代码_几个简单的python程序分享
  15. cad指示箭头快捷键命令_47个快捷键+50个CAD技巧助你玩转CAD
  16. 安卓逆向学习 之 KGB Messenger的writeup(2)
  17. 台湾成功大学起诉苹果Siri专利侵权 库克哥凌乱了
  18. android 属性动画伸缩,Android动画开发——Animation动画效果详解
  19. 二十四节气-春分。昼夜平分,日渐长~
  20. 成都计算机专科学院分数线,成都计算机工业职业技术学校2019年招生录取分数...

热门文章

  1. Java、C时间差值计算器(自用)
  2. 持续集成之jenkins插件管理及镜像源替换
  3. html文档中用于表示页面标题的标记对是,汽车发动机拆装与检修实训超星尔雅答案...
  4. centos DNS问题(只能ping通IP域名白费)
  5. 【经验分享(续篇)】Trachtenberg system(特拉亨伯格速算系统)
  6. WordPress主题模板主题巴巴博客X无限制版
  7. WordPress Auto Post 3.7 无限制版本
  8. 《Spatially and Temporally Efficient Non-local Attention Netw......》翻译文献--学习网络
  9. 报错集“nginx: [emerg] unknown directive “set_real_ip_from“ in /usr/local/nginx/conf/nginx.conf:50 ngi
  10. python怎么读write_Python读写文件