进程

进程

程序与进程

进程结构一般由三部分组成:代码段、数据段和堆栈段。代码段用于存放程序代码数据,数个进程可以共享一个代码段。而数据段存放程序的全局变量、常量和静态变量。堆栈段中栈用于函数调用,它存放着函数参数、函数内部定义的局部变量。对斩断还包含了进程控制块(PCB)。PCB处于进程核心堆栈底部,不需要额外分配空间。PCB是进程存在的唯一标识。系统通过PCB的存在而感知进程的存在。

程序如何转换为进程? 一般情况下linux下C++的生成分为四个阶段:预编译、编译、汇编、连接。编译器g++经过预编译、编译、汇编3个步骤将源程序将文件转换为目标文件。最后形成可执行程序。当程序执行时,操作系统将可执行程序复制到内存中

1 内核将程序将程序读入内存、为程序分配内存空间

2 内核为进程分配进程标识符(PID)及其他资源

3 内很为进程保存 PID 及其他信息,把进程放入运行队列中等待执行,程序转化为进城后就可以被调度执行。

进程的创建与结束

进程创建有两种方式:一种由操作系统创建,一种由父进程创建

系统启动时, 操作系统会创建一些进程,他们承担管理和分配系统资源的任务,这些进程被称为系统进程。系统允许一个进程创建新进程(子进程),子进程还可以创建新的进程,形成树型结构。整个linux的所有进程也是一个树形结构。树根由系统自动构造,即内核态下的0号进程,它是所有进程的祖先。0号进程创建1号进程,负责执行内核部分的初始化及系统配置,并创建告诉缓存和虚拟储存管理的内核线程。随后1号进程调用 execve() 运行可执行程序 init , 并演化成用户态的1号进程 它按照init/initab 的要求完成启动工作,创建若干终端注册进程getty() 用于用户的登录。

进程的创建 – fork

linux系统允许任何一个用户创建子进程,创建成功后,子进程在系统中并独立于父进程。该子进程接受系统调度,与父进程有相同的权利。

1
2
@include <unistd.h>
pid_t fork(void);

父进程和子进程:除了0号进程,linux系统中其他任何一个进程都是其他进程创建的。而相对的 ,fork 函数的调用方是父进程, 而创建的新进程是子进程。

fork 函数不需要参数,返回值是一个进程标识符

1 对于父进程,fork函数返回创建子进程ID

2 子进程 fork 函数返回0

3 创建出错的话 fork 函数返回 -1

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
pid_t pid;
pid = fork();
if(pid<0){
perror("fail to fork");
exit(-1);
}else if(pid ==0){
printf("Subprocess, PID: %u", getpid());
}else{
printf("Parentprocess, PID: %u", getpid());
}
return 0;
}

子进程和父进程共享数据段和堆栈段中的内容。例子略

事实上,子进程完全复制了父进程的地址空间,包括堆栈段和数据段。但是,子进程并未复制代码段,而是公用代码段。

进程的结束 – exit

僵尸进程

在linux中 正常情况下子进程是通过父进程创建的, 子进程和父进程的运行是一个异步的过程。父进程无法预料子进程在何时结束,于是就产生了孤儿进程和僵尸进程。

孤儿进程,是指一个父进程退出后,它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1) 到进程所收养,并由init进程完成状态收集工作。

僵尸进程,是指一个进程使用fork创建子进程,如果子进程退出,而父进程没有用wait或waitpid调用子进程的状态信息,子进程的进程描述符仍在系统中,这种进程被称为僵尸进程。

简单理解为,孤儿是父进程已退出而子进程未退出;而僵尸进程是父进程未退出而子进程先退出。

为了避免僵尸进程,需要父进程通过wait函数来回收子进程

守护进程

linux系统中在系统引导时会开启很多服务,这些服务就叫做守护进程。为了增加灵活性,root可以选择开启的模式,这些模式叫做运行级别。守护进程是脱离于终端在后台运行的进程,守护进程脱离终端是为了避免进程在执行过程中在终端上显示并且不会被终端的信息打断。

守护进程是一个生存期较长的进程,通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。说话进程常常在系统引导装入时启动,linux系统有很多的守护进程,大多数服务都是通过守护进程实现的。如作业规划进程、打印进程。

在 linux 中每一个与用户交流的界面称为终端,每一个终端开始的进程都会依附于该终端,这个终端就被称为 进程的控制终端,当控制终端被关闭时,相应的进程都会被自动关闭。但是守护进程可以突破这种限制,它从被执行时开始运转,整个系统关闭时才推出。如果想让某个进程不因为用户或终端等变化受到影响,那么就需把一个进程变成一个守护进程。

创建一个守护进程的步骤如下所示:

1 创建子进程,父进程退出。

这是编写守护进程的第一步。由于守护进程脱离终端控制,因此在第一步完成后就会在终端里造成程序已经运行完毕的假象。之后所有的工作都在子进程完成,而与用户终端脱钩。

2 子进程中创建会话

这个步骤是创建守护进程最重要的一步,虽然他的实现十分简单,但是他的意义重大。这里使用的系统函数setid, 这里有两个概念:进程组和会话期

1) 进程组: 一个或多个进程的集合。进程组由进程组ID来唯一标识,除了进程号以外,进程组ID也是一个进程的必备属性,每个进程组都有一个组长进程,组长进程号等于进程组ID,而且进程组ID不会因为组成进程的退出而受到影响。

2) 会话周期: 会话期是一个或多个进程组的集合。通常一个会话开始于用户登录,终止于用户退出,再次期间运行的所有进程都输入这个会话期。

setid 函数用于创建一个新的会话,并担任该会话组的组长,调用 setid 有三个作用: 1 让进程摆脱原会话的控制 2 让进程摆脱原进程组的控制 3 让进程摆脱原控制终端的控制。

那么 创建守护进程为什么需要setid函数? 这是由于创建守护进程的第一步掉用了fork函数来创建子进程,再将父进程退出。由于调用fork函数时子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但是会话期、进程组、控制终端等还没有改变,所以还不是真正意义上的独立。

3) 改变当前目录为根目录

这一步骤也是必要的步骤,使用 fork 创建的子进程集成了父进程的工作目录,由于进程的运行过程中当前的目录是不能卸载的,这对于以后的使用造成诸多麻烦。通常的做法是将 “/” 变为守护进程的当前工作目录,这样就可以避免上述问题。

4) 重设文件权限掩码

文件权限掩码是指屏蔽掉文件权限中的对应位。

5) 关闭文件描述符

同文件权限码一样, 用fork函数创建的子进程会从父进程哪里继承一些已经打开的文件,这些文件节能永远不会被守护进程读写,但是他们一样消耗系统资源。所以需要关闭来自继承的文件描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXFILE 65535
int main(){
pid_t pc;
int i, fd, len;
char* buf = "this is a Dameon\n";
len = strlen(buf);
pc = fork();
if(pc<0){
printf("error fock\n");
exit(1);
}else if(pc>0){
exit(0);
}
setsid();
chdir("/");
umask(0);
for(int i=0; i<MAXFILE; i++){
close(i);
}
while(1){
if(fd = open('/temp/demeon.log',O_CREAT|O_WRONLY|O_APPEND,0600))<0){
perror("open");
exit(1);
}
write(fd, buf, len+1);
close(fd);
sleep(10);
}
return 0;
}