多进程编程(一):基本概念

1、基本概念

  • 进程: 一个正在执行的应用程序,是操作系统 资源分配 的基本单元
  • 线程: 是正在执行的应用程序里面的一个具体任务,是操作系统 资源调度 的基本单元
  • 进程和线程间的关系:
    进程可以包括很多个线程,为线程提供必备的资源,所有的线程共享这些资源。每个线程负责完成一个具体的任务,线程间相互配合,共同保证进程正常运行。同时,线程也有自己的私有资源。

2、进程的状态

  • 就绪态、运行态、阻塞态
    在这里插入图片描述
    运行态: 进程占有处理器正在运行
    就绪态: 进程具备运行的条件,正在等待系统分配处理器,然后开始运行
    阻塞态: 进程不能够直接运行,正在等待某个事件结束,然后进入就绪态

Linux环境下,查看筛选进程的shell命令

  • ps -ef :查看系统的全部进程
  • ps aux :查看系统的全部进程
  • ps -ef | grep demo :从全部进程中,筛选出和 demo 相关的进程
  • 杀死进程:kill -9 pid_t

3、创建进程

fork()
  • 必备头文件
    #include <sys/types.h>
    #include <unistd.h>
    
  • 用于产生一个子进程,函数返回值pid_t是一个整数,在父进程中,返回值是子进程编号;在子进程中,返回值是0。如果创建失败,则返回 -1。
    pid_t fork(void);
    
  • 特点:
    1、fork()函数所创建的子进程是父进程的完整副本,复制了父进程的资源
    写时复制:刚创建子进程后,父子进程对于变量是共享的,只要在任一进程对数据执行了写操作时,复制才会发生(数据就不共享了,先是缺页中断,然后操作系统给子进程分配内存,并复制父进程的数据)。
    2、子进程拥有自己的虚拟地址空间,父子进程数据独有,代码共享(fork()函数后的代码
    3、根据返回值,来判断是父进程还是子进程
  • 举例:
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main() {
          
          
    	pid_t res;
    	res = fork();
    
    	if (res > 0) {
          
          
    		printf("This is parent, pid = %d\n", getpid());
    	} else if (res == 0) {
          
          
    		printf("This is child, pid = %d\n", getpid());
    	} else {
          
          
    		perror("fork");
    	}
    
    	return 0;
    }
    
    打印输出
    This is parent, pid = 3543
    This is child, pid = 3544
    
vfork()
  • 必备头文件

    #include <sys/types.h>
    #include <unistd.h>
    pid_t vfork(void);
    
  • 特点:
    1、返回值和fork()函数相同。
    2、vfork()不创建子进程的虚拟地址,直接共享父进程的,从而物理地址也被共享了
    3、子进程先运行,在子进程调用 exec(进程替换)或者exit之后,父进程被调度执行

  • 举例:

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main() {
          
          
    	int num = 10;
    
    	pid_t res;
    	res = vfork();
    
    	if (res > 0) {
          
          
    		printf("This is parent, pid = %d\n", getpid());
    		num += 10;
    		printf("num = %d\n", num);
    
    	} else if (res == 0) {
          
          
    		printf("This is child, pid = %d\n", getpid());
    		num += 10;
    		printf("num = %d\n", num);
    		exit(0);
    	} else {
          
          
    		perror("vfork");
    	}
    
    	return 0;
    }
    

    打印输出

    This is child, pid = 4495
    num = 20
    This is parent, pid = 4494
    num = 30
    

    从输出结果可以看出,子进程先执行,在子进程调用 exit(0)退出之后,父进程再执行。
    并且,父子进程共享 num变量。

fork()vfork()的不同之处:
  • fork()复制父进程的页表项,当进行写操作时,内核给子进程分配一个新的内存页面
    vfork()与父进程共享页表项,当写操作时,直接写在父进程的内存页面
  • fork()创建的子进程与父进程之间的执行次序不确定
    vfork()是子进程先运行,在子进程调用 exec(进程替换)或者exit之后,父进程被调度执行
  • vfork()保证子进程先运行,在子进程调用execexit之后父进程才可能被调度运行。如果在
    调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

4、exec函数族

有时候,我们需要在子进程中执行其它程序,即替换当前进程映像,这时候会使用exec函数族中的一些函数。
**作用:**根据指定的文件名找到可执行文件,并用它来取代调用进程的内容(在调用进程内部执行一个可执行文件)
返回值: exec函数族的函数执行成功后不会返回,只有调用失败了,才会返回-1,回到源程序的调用点接着往下执行。

常用函数:

  • 1、

    int execl(const char *pathname, const char *arg, ...);
    - pathname: 要执行的可执行文件的路径
    - arg: 执行可执行文件所需要的参数列表
    	arg一般填写当前执行程序的名字,后面依次是可执行程序所需要的参数列表,参数最后要以 NULL 结束。
    - 返回值:
    	当调用失败时,才有返回值,返回-1
  • 2、

    int execlp(const char *file, const char *args, ...);
    - 和 execl 基本一致,不过对于file会从环境变量中寻找。
    

5、孤儿进程

定义: 父进程结束运行,但是子进程还在运行,这样的子进程为孤儿进程(父死子在)。
注意: 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init,而 init进程会循环地 wait() 它的已经退出的子进程。这样当一个孤儿进程结束生命周期的时候,init()进程就会处理它的一切善后工作。
在子进程退出的时候, 进程中的用户区可以自己释放, 但是进程内核区的PCB资源自己无法释放,必须要由父进程来释放子进程的PCB资源,孤儿进程被领养之后,这件事儿init进程就可以代劳了,这样可以避免系统资源的浪费。
因此,孤儿进程并不会有什么危害。

6、僵尸进程

在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行,子进程先与父进程结束,子进程无法释放自己的PCB资源,需要父进程来做这个件事儿,但是如果父进程也不管,这时候子进程就变成了僵尸进程。
僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡了,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。僵尸进程的出现是由于这个已死亡的进程的父进程不作为造成的。

内核资源有限,不允许产生大量的僵尸进程。
僵尸进程不能被 kill -9 杀死。
可以在父进程中使用 wait() 或者 waitpid() 函数,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
- 功能:等待任意一个子进程结束,并回收子进程。
- 参数:wstatus, 进程退出时的状态信息,传入的是一个int类型的指针(传出参数)
- 返回值:
	成功:返回被回收的子进程的id
	失败:-1(所有子进程都结束,调用函数失败)

注意: 调用 wait() 函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒。
如果没有子进程了,函数立刻返回-1;
如果子进程都已经结束了,也会立即返回,返回 -1。

猜你喜欢

转载自blog.csdn.net/Sir666888/article/details/125454930