Linux - 进程

程序与进程

  1. 程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁....)。
  2. 进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源,在内存中执行。(程序运行起来,产生一个进程)

并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但任一个时刻点上仍只有一个进程在运行。

  1. 单道程序设计:所有进程排队执行。
  2. 多道程序设计:多个进程分时复用CPU资源(利用时钟中断,硬件实现)。

CPUandMMU

MMU:内存管理单元,1-3G为用户区user,3-4G为内核区kernel;

两个a.out对应user通过MMU映射到物理内存上的不同位置,且最小单位page为4K, 对应kernel映射到同一个位置,即同用一块内存空间,但是两个PCB不一致。

我们在程序中使用的都是虚拟内存,由MMU完成虚拟内存与物理内存的映射关系。

MMU

进程控制块PCB

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
  • 进程的状态,有就绪、运行、挂起、停止等状态。
  • 进程切换时需要保存和恢复的一些CPU寄存器。
  • 描述虚拟地址空间的信息。
  • 描述控制终端的信息。
  • 当前工作目录(Current Working Directory)。
  • umask掩码。
  • 文件描述符表,包含很多指向file结构体的指针。
  • 和信号相关的信息。
  • 用户id和组id。
  • 会话(Session)和进程组。
  • 进程可以使用的资源上限(Resource Limit)。

进程状态

进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。

PCB_state

进程控制

注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。且父进程返回的是子进程pid,子进程返回0。

创建子进程

#include<unistd.h>
#include<iostream>

using namespace std;

int main()
{
 cout<<"Test Func begin!"<<endl;
 pid_t m_pid;
 m_pid= fork();

 if(m_pid == -1)
{
  perror("fork fail.");
}
 else if(m_pid == 0)
{
  cout<<"I'm child "<<"id:"<<getpid()<<" ppid:"<<getppid()<<endl;
}
 else if(m_pid > 0)
{
  cout<<"I'm parent "<<"id:"<<getpid()<<" ppid:"<<getppid()<<" childID:"<<m_pid<<endl;
  sleep(2);
}

 cout<<"fork end"<<endl;

 return 0;
}
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

## 循环创建子进程

#include<unistd.h>
#include<iostream>

using namespace std;

int main()
{
   cout<<"Test Func begin!"<<endl;
   pid_t m_pid;
   int i;
   for(i=0;i<5;i++)
   {
      m_pid= fork();
      if( m_pid == 0)
         break;
   }
    sleep(i);
    if( i < 5)
    {
       cout<<"I'm child "<<i<<" id:"<<getpid()<<endl;
    }
    else
    {
       cout<<"I'm parent "<<" id:"<<getpid()<<endl;
       cout<<"Test Func end!"<<endl;
    }

 return 0;
}
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

进程共享

刚fork之后:

  1. 父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...
  2. 父子不同处: 1.进程ID   2.fork返回值   3.父进程ID    4.进程运行时间     5.闹钟(定时器)   6.未决信号集
  3. 似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?

注意:父子进程间遵循读时共享写时复制的原则。父子独享0-3G用户区。

父子进程共享:1. 文件描述符(打开文件的结构体)  2. mmap建立的映射区 (进程间通信详解)

exec函数族

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。

exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。

常用的如下:

execlp("ls", "ls", "-l", "-F", NULL);         使用程序名在PATH中搜索。
execl("/bin/ls", "ls", "-l", "-F", NULL);    使用参数1给出的绝对路径搜索。
1
2

进程内容

孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。

僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。(需要死父进程进行回收)

wait函数

父进程调用wait函数可以回收子进程终止信息。该函数有三个功能: ① 阻塞等待子进程退出  ② 回收子进程残留资源  ③ 获取子进程结束状态(退出原因)。     pid_t wait(int *status);     成功:清理掉的子进程ID;失败:-1 (没有子进程)

waitpid函数

pid_t waitpid(pid_t pid, int *status, in options);    成功:返回清理掉的子进程ID;失败:-1(无子进程)
特殊参数和返回情况:
参数pid: 
> 0 回收指定ID的子进程    
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
< -1 回收指定进程组内的任意子进程

参2:状态。

参3: 0:  (wait) 阻塞回收
     WNOHANG:   非阻塞回收   (轮询)

返回值:   成功: pid  失败:-1     返回0:3传WNOHANG, 并且子进程尚未结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14

进程间通信

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。常用的进程间通信方式有:

  • 管道 (使用最简单)
  • 信号 (开销最小)
  • 共享映射区 (无血缘关系)
  • 本地套接字 (最稳定,最常使用)

管道

管道是一种最基本的IPC机制,**作用于有血缘关系的进程之间,完成数据传递。**调用pipe系统函数即可创建一个管道。有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区)
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出。

管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

管道的局限性:

  1. 数据自己读不能自己写。
  2. 数据一旦被读走,便不在管道中存在,不可反复读取。
  3. 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
  4. 只能在有公共祖先的进程间使用管道。

常见的通信方式有,单工通信、半双工通信、全双工通信。

测试程序:fork两个子进程,兄负责写ls,弟负责读:拼接为ls |wc -l的执行逻辑。

  • pid_t fork(void); 失败返回-1;成功返回:父进程返回子进程的ID(非负) ; 子进程返回 0
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
	pid_t pid;
	int fd[2], i;
	
	pipe(fd);

	for (i = 0; i < 2; i++) {
		if((pid = fork()) == 0) {
			break;
        }
    }

	if (i == 0) {			//兄
		close(fd[0]);				//写,关闭读端
		dup2(fd[1], STDOUT_FILENO);		
		execlp("ls", "ls", NULL);	
	} else if (i == 1) {	//弟
		close(fd[1]);				//读,关闭写端
		dup2(fd[0], STDIN_FILENO);		
		execlp("wc", "wc", "-l", NULL);		
	} else {
        close(fd[0]);
        close(fd[1]);
		for(i = 0; i < 2; i++)		//两个儿子wait两次
			wait(NULL);
	}

	return 0;
}
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

FIFO

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。

FIFO是Linux基础文件类型中的一种。但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。

创建方式:

    fd = open(argv[1], O_WRONLY);
    if (fd < 0) 
        sys_err("open");

    i = 0;
    while (1) {
        sprintf(buf, "hello itcast %d\n", i++);

        write(fd, buf, strlen(buf));
        sleep(1);
    }

[root@192 fifo]# ./fifo_w klc.txt


    fd = open(argv[1], O_RDONLY);
    if (fd < 0) 
        sys_err("open");
    while (1) {
        len = read(fd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, len);
        sleep(3);           
    }

[root@192 fifo]# ./fifo_r klc.txt
hello itcast 0
hello itcast 1
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

信号

共享映射区 (无血缘关系)

  1. 文件进程间通信:
  • 使用文件也可以完成IPC,理论依据是,fork后,父子进程共享文件描述符。也就共享打开的文件。 #include <fcntl.h> fd2 = open("test.txt", O_RDWR);//父子进程打开同一个文件
  1. 存储映射I/O 函数:
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset); 
返回:成功:返回创建的映射区首地址;失败:MAP_FAILED宏
int munmap(void *addr, size_t length);    成功:0; 失败:-1
int ftruncate(int fd, off_t length);
1
2
3
4

截断文件为特定长度,作为mmap的length传入。

  1. mmap父子进程通信
  • 父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
    • MAP_PRIVATE:  (私有映射)  父子进程各自独占映射区;
    • MAP_SHARED:  (共享映射)  父子进程共享映射区;
  1. 匿名映射

  2. mmap无血缘关系进程间通信

  • 实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。