进程和线程

本文详细介绍了进程和线程的概念、状态转换、创建、控制及同步互斥,包括进程的PCB、fork、vfork、exec函数族,线程的pthread_create、pthread_join、互斥锁和信号量等,探讨了进程与线程的区别和使用场景。
摘要由CSDN通过智能技术生成

do_fork 的实现 (qq.com)

一、进程和线程

进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构)

线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

“进程——资源分配的最小单位,线程——程序执行的最小单位”

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

二、进程

1、进程概述

1.1 进程的定义

程序和进程的区别:

​ 程序:是静态的,存放在磁盘上的可执行文件
​ 进程:是动态的,是运行在内存中的程序的执行实例

程序是一些指令的有序集合,而进程是程序执行的过程,进程是程序的一次执行过程。进程的状态是变化的,其包括进程的创建、调度和消亡。
只要程序运行,此时就是进程,程序每运行一次,就会创建一个进程

在linux系统中,进程是管理事务的基本单元。

进程拥有自己独立的处理环境和系统资源(处理器、存储器、I/O设备、数据、程序)。

1.2 进程的状态及转换

进程整个生命周期可以简单划分为三种状态:

就绪态:
进程已经具备执行的一切条件,正在等待分配CPU的处理时间。

执行态:
该进程正在占用CPU运行。

等待态:
进程因不具备某些执行条件而暂时无法继续执行的状态。

进程的调度进制:
时间片轮转,上下文切换
多进程不是说一个进程执行完再执行另一个进程,而是交替执行的,一个进程执行一段时间,然后下一个进程在执行一段时间,依次类推,所有进程执行完之后再回到第一个进程初继续执行以此类推

进程三个状态的转换关系

第三章 进程

1.3 进程控制块

进程控制块就是用于保存一个进程信息的结构体,又称之为PCB

OS是根据PCB来对并发执行的进程进行控制和管理的。系统在创建一个进程的时候会开辟一段内存空间存放与此进程相关的PCB数据结构。
PCB是操作系统中最重要的记录型数据结构。PCB中记录了用于描述进程进展情况及控制进程运行所需的全部信息。
PCB是进程存在的唯一标志,在Linux中PCB存放在task_struct结构体中。

task_struct结构体保存在/usr/src/linux-headers-4.4.0-176-generic/include/linux/sched.h 一般在1500或者1300行左右

PCB结构体中的部分数据

调度数据
​ 进程的状态、标志、优先级、调度策略等。

时间数据
​ 创建该进程的时间、在用户态的运行时间、在内核态的运行时间等。

文件系统数据
​ umask掩码、文件描述符表等。
​ 内存数据、进程上下文、进程标识(进程号)

2、进程控制

2.1 进程号

2.1.1 ps ajx 查看进程

每个进程都由一个进程号来标识,其类型为pid_t,进程号的范围:0~32767
进程号是由操作系统随机给当前进程分配的,不能自己控制
进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用了

在ubuntu中查看当前系统中所有的开启的进程

ps ajx
     
     PPID:当前进程的父进程的进程号
	PID:当前进程的进程号
	PGID:当前进程所在的组的进程组ID
	COMMAND:当前进程的名字

image-20210508143657448

特殊的进程号:
在linux系统中进程号由0开始。
进程号为0及1的进程由内核创建。
进程号为0的进程通常是调度进程,常被称为交换进程(swapper)。
进程号为1的进程通常是init进程,init进程是所有进程的祖先。
除调度进程外,在linux下面所有的进程都由进程init进程直接或者间接创建。

进程号(PID)
标识进程的一个非负整型数。

父进程号(PPID)
任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。

进程组号(PGID)
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID) 。

Linux操作系统提供了三个获得进程号的函数getpid()、getppid()、getpgid()。

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:获取当前进程的进程号
pid_t getppid(void);
功能:获取当前进程的父进程的进程号
pid_t getpgid(pid_t pid);
功能:获取当前进程所在进程组的id

案例

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
   
    //获取当前进程的进程号
    printf("pid = %d\n", getpid());

    //获取当前进程的父进程的id
    printf("ppid = %d\n", getppid());

    //获取当前进程所在组的id
    printf("pgid = %d\n", getpgid(getpid()));

    while(1)
    {
   

    }

    return 0;
}

执行结果

第三章 进程

使用ps ajx查询的结果

第三章 进程

2.2 进程的创建

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

功能:在已有的进程基础上又创建一个子进程
参数:
	无
返回值:
	成功:
		>0 子进程的进程号,标识父进程的代码区
		0 子进程的代码区
	失败:
		‐1 返回给父进程,子进程不会创建
     
关于pid_t:
     
     pid_t是一个typedef定义类型。
	用它来表示进程id类型。

sys/types.h:
typedef short    pid_t;       /* used for process ids */

	pid_t就是一个short类型变量,实际表示的是内核中的进程表的索引

	头文件里也不过是个typedef而已.
	使用pid_t而不使用int只是为了可移植性好一些.
	因为在不同的平台上有可能
	typedef int pid_t
	也有可能
	typedef long pid_t

使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间

地址空间:
包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号
等。
子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的。

fork函数执行完毕后父子进程的空间示意图:

第三章 进程

2.2.1 创建子进程

不区分父子进程(不推荐)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
   
     //通过fork函数创建一个子进程
     //注意:主要执行一次fork,就会在原有的进程基础上创建一个新的子进程
     //而且如果fork之后不区分父子进程的代码区,则后面所有的代码都会执行
     fork();
     printf("hello world\n");
     
     while(1);
     return 0;
}

执行结果

第三章 进程

区分父子进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
   
	//通过fork函数创建一个子进程

	//通过fork函数的返回值来区分父子进程的独立的代码区
	//父子进程是来回交替执行的,谁先运行,谁后运行是不确定的,不要认为父进程执行完之后才会执行子进程
	pid_t pid;

	pid = fork();
	if(pid < 0)
	{
   
		perror("fail to fork");
		return -1;
	}
	else if(pid > 0) //父进程的代码区
	{
   
		while(1)
		{
   
			printf("parent: pid = %d, ppid = %d\n", getpid(), getppid());
			printf("pid = %d\n", pid);
			printf("this is a parent process\n");
			sleep(1);
			printf("****************\n");
		}
	}
	else //子进程的代码区
	{
   
		while(1)
		{
   
			printf("son: pid = %d, ppid = %d\n", getpid(), getppid());
			printf("this is a son process\n");
			sleep(1);
			printf("-----------------\n");
		}
	}

	return 0;
}

执行结果

第三章 进程

ps ajx结果

第三章 进程

2.2.2 父子进程拥有独立的地址空间
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int a = 666;

int main(int argc, char *argv[])
{
   
	pid_t pid;
	static int b = 777;
	int c = 888;

	//子进程会复制父进程fork之前的所有内容
	//但是fork之后,父子进程完全独立,所以不管双方怎么改变(堆区、栈区、数据区等),都不会收对方影响

	pid = fork();
	if(pid < 0)
    {
   
		perror("fail to fork");
        return -1;
    }
	if(pid > 0)  //父进程的代码区
	{
   
		printf("This is a parent process\n");
		a++;
		b++;
		c++;
		printf("a = %d, b = %d, c = %d\n", a, b, c);
	}
	else  //子进程的代码区
	{
    
		sleep(1);
		printf("This is a son process\n");
		printf("a = %d, b = %d, c = %d\n", a, b, c);
	}

	while(1)
	{
   

	}
    
	return 0;
}

执行结果

第三章 进程

2.2.3 子进程继承父进程的空间
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
   
	int fd;
	if((fd = open("file.txt", O_RDONLY)) == -1)
	{
   
		perror("fail to open");
		return -1;
	}

	//子进程会继承父进程的一些公有的区域,不如磁盘空间,内核空间
	//文件描述符的偏移量保存在内核空间中,所以父进程改变偏移量,则子进程获取的偏移量是改变之后的
	pid_t pid;
	pid = fork();
	if(pid < 0)
    {
   
		perror("fail to fork");
        return -1;
    }
	if(pid > 0)
	{
   
		printf("This is a parent process\n");

		char buf[32] = "";
		if(read(fd, buf, 30) == -1)
		{
   
			perror("fail to read");
			return -1;
		}

		printf("buf = [%s]\n", buf);

	}
	else 
	{
   
		sleep(1);
		printf("This is a son process\n");

		char buf[32] = "";
		if(read(fd, buf, 30) == -1)
		{
   
			perror("fail to read");
			return -1;
		}

		printf("buf = [%s]\n", buf);
	}

	while(1)
	{
   

	}
    
	return 0;
}

执行结果

第三章 进程

2.3 进程的挂起

2.3.0 sleep()

#include <unistd.h>
unsigned int sleep(unsigned int seconds);

功能:进程在一定的时间内没有任何动作,称为进程的挂起(进程处于等待态)
参数:
	seconds:指定要挂起的秒数
返回值:
	若进程挂起到sec指定的时间则返回0,若有信号中断则返回剩余秒数
注意:
	进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到就绪态

案例

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

int main(int argc, char const *argv[])
{
   
    while(1)
    {
   
        printf("hello world\n");

        //当运行到sleep函数后,程序会在此位置等待设定的秒数,当秒数到大后,代码会接着执行
        //sleep运行时进程为等待态,时间到达后会先切换到就绪态,如果代码继续运行,再切换到运行态
        sleep(2);
    }
    
    return 0;
}

执行结果

第三章 进程

2.4 进程的等待

2.4.1 wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

功能:等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
	调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。
     若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
参数:
	status:函数返回时,参数status中包含子进程退出时的状态信息。
	子进程的退出信息在一个int中包含了多个字段,
	用宏定义可以取出其中的每个字段
	子进程可以通过exit或者_exit函数发送退出状态
返回值:
	成功:子进程的进程号。
	失败:‐1

取出子进程的退出信息

WIFEXITED(status)
​ 如果子进程是正常终止的,取出的字段值非零。
WEXITSTATUS(status)
​ 返回子进程的退出状态,退出状态保存在status变量的8~16位。
​ 在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。

注意:
此status是个wait的参数指向的整型变量。

案例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
   
	pid_t pid;
	
	pid=fork();
	if(pid<0)
	{
   
		perror("fail to fork");
        return -1;
    }
	if(pid == 0)
	{
   
		int i = 0;
		for(i=0;i<5;i++)
		{
   
			printf("this is son process\n");
			sleep(1);
		}

		//使用exit退出当前进程并设置退出状态
		exit(2);
	}
	else 
	{
   
		//使用wait在父进程中阻塞等待子进程的退出
		//不接收子进程的退出状态
		//wait(NULL);

		//接收子进程的退出状态,子进程中必须使用exit或者_exit函数退出进程是发送退出状态
		int status = 0;
		wait(&status);

		if(WIFEXITED(status) != 0)
		{
   
			printf("The son process return status: %d\n", WEXITSTATUS(status));
		}

		printf("this is father process\n");	
	}

	return 0;
}

执行结果

第三章 进程

2.4.2 waitpid()
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status,int options)

功能:
	等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数pid 的值有以下几种类型:
	pid>0:
		等待进程ID 等于pid 的子进程。
	pid=0
		等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
	pid=-1:
		等待任一子进程,此时waitpid 和wait 作用一样。
	pid<-1:
		等待指定进程组中的任何子进程,这个进程组的ID 等于pid 的绝对值。
status 参数中包含子进程退出时的状态信息。
options 参数能进一步控制waitpid 的操作:
	0:
		同wait,阻塞父进程,等待子进程退出。
	WNOHANG:
		没有任何已经结束的子进程,则立即返回。
	WUNTRACED
		如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(跟踪调试,很少用到)
返回值:
	成功:
		返回状态改变了的子进程的进程号;如果设置了选项WNOHANG 并且pid 指定的进程存在则返回0。
	出错:
		返回-1。当pid 所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid 就会出错返回,这时errno 被设置为ECHILD。
          
wait(status) <==> waitpid(1, status, 0)

案例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
   
	pid_t pid;
	
	pid=fork();
	if(pid < 0)
    {
   
		perror("fail to fork");
        return -1;
    }
	if(pid == 0)
	{
   
		int i = 0;
		for(i=0;i<5;i++)
		{
   
			printf("this is son process\n");
			sleep(1);
		}
		exit(0);
	}
	else 
	{
   		
		waitpid(pid, NULL, 0);
		printf("this is father process\n");	
	}
	return 0;
}

执行结果

![第三章 进程](https://gitee.com/an_zhiyang/nancheng/raw/master/master/Linux/20210508152213.jpg)

### 2.5 进程的终止

#### 2.5.1 exit()

​```c
#include <unistd.h>
void _exit(int status);
功能:退出当前进程
参数:
	status:退出状态,由父进程通过wait函数接收这个状态
	一般失败退出设置为非0
	一般成功退出设置为0
返回值:
	无
2.5.2 _exit()
#include <stdlib.h>
void exit(int status);
功能:退出当前进程
参数:
	status:退出状态,由父进程通过wait函数接收这个状态
	一般失败退出设置为非0
	一般成功退出设置为0
返回值:
	无
exit和_exit函数的区别:
	exit为库函数,而_exit为系统调用
	exit会刷新缓冲区,但是_exit不会刷新缓冲区
	一般会使用exit

第三章 进程

案例

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

void myfun()
{
   
    printf("nihao beijing");

    //使用return
    //return除了可以返回值以外,在主函数中使用可以退出进程,但是在子函数中使用只能退出当前函数
    //return ;

    //使用exit
    //exit可以退出一个进程并且可以刷新缓冲区
    exit(0);
    
    //使用_exit
    //_exit可以退出一个进程,但是不会刷新缓冲区
    //_exit(0);

    printf("welcome to 1000phone\n");
}

int main(int argc, char const *argv[])
{
   
    printf("hello world\n");

    myfun();

    printf(
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南 城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值