目录
1、进程(Process)
1.1 进程基本概念:
1.2 进程分类
1.3 进程的特征
1.4 进程和程序的区别
1.5 进程的状态
1.6 进程的创建——Fork()函数
1.6.1 简介
1.6.2 使用
1.7 进程终止
2、线程(Thread)
1.1 线程基本概念:
1.2 线程的一些相关知识
1.3 多线程的缺点
1.4 创建线程
1.4.1 创建线程 pthread_create
1.4.2 结束线程 pthread_exit
1.4.3 线程等待 pthread_join
1.4.4 线程分离 pthread_detach
1.4.5 助理解线程等待函数和线程分离函数
通俗理解
3、进程和线程的区别
3.1 区别
3.2 举例
3.3 总结
学习Linux的同学都知道,进程、线程这两个东西,有些同学理解能力强的看书上的概念就可以理解是什么,或者有些初学者,怎么看都看不明白;别担心,下面我将分别讲解进程、线程的概念再举相应的生活中的例子助理解,废话不多说,开始吧。
1、进程(Process)
有些童鞋觉得进程离我们很远,其实不然,我们可以随时看到,最直观的是打开我们电脑任务管理器。如下图:
1.1 进程基本概念:
对于进程的概念,我讲出三种概念,那种便于理解就理解那种,希望能够帮助需要的人。
① 描述和管理程序的"运行过程"称为进程。在Windows中可以打开任务管理器查看进程。
② 进程是程序在计算机上的一次执行活动。一个运行的程序,可能有多个进程。进程在操作系统中执行特定的任务。进程是正在执行的一个程序的实例,通常是由程序,数据集合和进程控制块三部分组成。
③进程是一个程序的执行实例,具有独立的内存空间和资源。操作系统为每个进程分配独立的资源,如内存、文件句柄等。进程之间是相互独立的,一个进程的崩溃不会影响其他进程。
为了便于理解,举一个生活中的例子,自行想象一下。
独立的房子:想象每个进程是一栋独立的房子,每栋房子有自己的院子、厨房、卧室等(内存和资源)。房子之间是独立的,一个房子的火灾不会直接影响到另一个房子。
1.2 进程分类
进程一般分为交互进程、批处理进程、守护进程。
①交互进程:是由shell启动的进程,它既可以在前台运行,也可以在后台运行。交互进程在执行过程中,交互进程在执行过程中,要求与用户进行交互操作。简单来说,就是用户需要给出某些参数或者信息,进程才能继续执行。
②批处理进程:与windows原来的批处理很类似,是一个进程序列。该进程负责按照顺序启动其它进程。
③守护进程:是执行特定功能或者执行系统相关任务的后台进程。守护进程只是一个特殊的进程,不是内核的组成部分。许多守护进程在系统启动时启动,直到系统关闭时才停止运行。而某些守护进程只是在需要时才会启动,比如FTP或者Apache服务等,可以在需要的时候才启动该服务。
1.3 进程的特征
1.动态性:进程是程序的一次执行过程,动态的产生,动态的消亡。
2.并发性:进程同其他进程一起向前推进。
3.异步性:每个进程按照自己各自的速度向前推进。
4.独立性:每个进程都是一个独立的实体,有自己独立的地址空间、资源和控制流。这意味着一个进程的执行不会直接影响其他进程的执行。
1.4 进程和程序的区别
1.动态与静态:进程是动态的,它是程序的一次执行过程;程序是静态的,它是一组指令的有序集合。
2.暂存与长存:进程是暂存的,它在内存上短暂驻留;程序是长存的,它在磁盘或者其他介质上长期保存。
3.程序和进程的对应关系:一个程序可能有多个进程(一个程序运行多次就会产生多个进程)。
1.5 进程的状态
1.运行状态(Running):进程已经占有CPU,并且在CPU上运行。
2.就绪状态(Ready):具有运行条件但没有CPU而暂时不能运行,处于就绪态的进程只要占有CPU便立马可以运行。
3.阻塞状态(Block):进程因为等待某项服务的完成或者信号而不得不停下来,例如调用系统调用等待执行结果、I/O操作操作、等待合作进程的信号......
实际上不同的操作系统有不同的进程状态,某些操作系统甚至具有新建态(new)和终止态(terminate):
Linux的进程状态:
1.可运行态:Linux没有就绪态,它把占有CPU的进程和处于就绪队列的进程的状态统称为可运行态。
2.阻塞态:Linux分为浅度阻塞和深度阻塞。浅度阻塞的进程可以被其他进程的信号或者时钟唤醒,反之深度阻塞的进程则不能。
3.僵尸态:进程终止运行时所处的状态,处于这个状态的进程会释放大部分资源。
4.挂起态:当调试程序时这个进程就处于挂起态。
1.6 进程的创建——Fork()函数
1.6.1 简介
fork()函数的功能是创建一个新的进程,新进程为当前进程的子进程,那么当前的进程为父进程。在一个函数中,可以通过fork()函数的返回值判断进程是在子进程中还是在父进程中。
①一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
②一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己
③fork()函数调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
(1)在父进程中,fork()函数返回新创建子进程的进程ID;
(2)在子进程中,fork()函数返回0;
(3)如果出现错误,fork()函数返回一个负值;
1.6.2 使用
//示例
#include <stdio.h>
#include <unistd.h> int main()
{pid_t pid;pid = fork(); //创建新进程if (pid == 0) //返回子进程{ printf("child pid: %d\n", getpid());} else {printf("pid: %d\n", pid);//父进程中返回子进程的pidprintf("father pid: %d\n", getpid());}
}
①一个父进程希望复制自己,使父子进程同时执行不同的代码段。
比如在网络服务程序中,父进程等待客户端的服务请求。当请求到达时,父进程调用fork()使子进程处理此请求;而父进程继续等待下一个请求。②一个进程要执行一个不同的程序。这个在shell下比较常见,这种情况下,fork()之后一般立即接exec函数。
③fork()函数出错可能有两种原因:
(1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
(2)系统内存不足,这时errno的值被设置为ENOMEM。③每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
④fork()函数执行完毕后,出现两个进程。
vfor()函数
(1)vfork
基本功能和fork相同
(2)区别:
vfork()函数创建新进程的主要目的是exec一个新程序。
vfork()函数并不复制父进程的地址空间,并和父进程的内存数据share一起用。
vfork()函数保证子进程先运行。
当子进程调用exit()或exec()后,父进程往下执行。
1.7 进程终止
进程终止的5种正常情况
①在main中执行return(在main函数中会结束当前进程;子函数中,会返回调用当前函数的调用位置,进程从下个C语句开始执行)
②调用_exit 或_Exit(结束当前的进程,不对缓存区刷新)
③调用exit(结束当前的进程,并且会刷新缓存区,关闭没有关闭的文件等)
④进程的最后一个线程执行了返回语句
⑤进程的最后一个线程调用了pthread_exit函数进程的3种异常终止方式
①调用abort,产生SIGABRT信号
②进程收到某些信号
③最后一个线程对“取消”请求作出响应
2、线程(Thread)
1.1 线程基本概念:
对于线程的概念,也是进程一样,讲多个概念。
①线程是进程中的一个执行单元,一个进程可以包含多个线程。线程共享进程的资源,如内存和文件句柄,但每个线程有自己的程序计数器、寄存器和栈。
②线程是包含在进程内。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,线程之间资源是共享的。
③线程是CPU直接运行的实体,是CPU调度的基本单位。一个进程可以有多个执行路径,这些路径叫做线程。多个线程可以同时共享CPU,从而实现并发。
生活中的例子:
房子里的房间:想象一个进程是一栋房子,线程是房子里的房间。房子里的房间(线程)共享房子的院子、厨房等(进程的资源),但每个房间有自己的家具(程序计数器和栈)。房间之间可以并行使用资源,但一个房间的操作不会直接影响到其他房间。
1.2 线程的一些相关知识
①如果只有一个线程,任务就是顺序执行的。一个进程可以多个线程,每条线程并行执行不同的任务,引入多线程可以在执行某个任务的过程中,执行其他任务。所以在耗时多任务中,应用非常广泛。
②线程通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是进程的表亲,同一进程中的多条线程将共享该进程中的全部系统资源。如:虚拟地址空间,文件描述符和信号处理等等
③线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。
④同一进程中的多个线程有各自的调用栈(call stack)、寄存器环境(register context)、本地存储(thread-local storage)。
⑤在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。
单线程程序:整个进程只有一个线程,不适用多线程技术时都是单线程程序,这个线程称为主线程。
多线程程序:整个进程有至少两个线程,多个线程当中一定存在一个主线程。
1.3 多线程的缺点
多线程并不是完美的,它也会带来许多让人头疼的问题。例如程序调试起来是非常困难的,因为是多个执行流并发执行的;并发的过程难以控制,因为CPU的调度是随机的,我们不能预测;线程安全问题,这也是最严重的问题,当多个线程同时访问同一份资源时,就会产生数据冲突、数据不一致等等问题。
1.4 创建线程
1.4.1 创建线程 pthread_create
创建线程是多线程编程的第一步,理解线程创建是多线程编程的关键;mian 函数运行时,系统会自动创建一个线程,称为主线程。通过 pthread_create创建的线程,称为子线程。创建线程的函数如下。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
线程创建函数包含四个变量,分别为:
1. 一个线程变量名,被创建线程的标识;
thread
:用于存储新线程标识符的变量。2.线程的属性指针,缺省为NULL即可;
attr
:用于设置新线程属性的指针,通常可以传入NULL
以使用默认属性。3.被创建线程的程序代码;
start_routine
:新线程的入口函数,是线程执行的起点。4. 程序代码的参数;
arg
:传递给入口函数start_routine
的参数。返回值
1.若线程创建成功,返回0。
2.若线程创建失败,返回非0的错误码。
pthread_create
函数的使用示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>// 线程函数
void* thread_function(void* arg)
{int thread_num = *((int*)arg);printf("Hello from thread %d\n", thread_num);pthread_exit(NULL);
}int main()
{pthread_t threads[5];int thread_args[5];int result_code;// 创建5个线程for (int i = 0; i < 5; ++i) {thread_args[i] = i;printf("Main: creating thread %d\n", i);result_code = pthread_create(&threads[i], NULL, thread_function, &thread_args[i]);if (result_code != 0) {printf("Error creating thread %d, error code: %d\n", i, result_code);exit(EXIT_FAILURE);}}// 等待所有线程结束for (int i = 0; i < 5; ++i) {result_code = pthread_join(threads[i], NULL);if (result_code != 0) {printf("Error joining thread %d, error code: %d\n", i, result_code);exit(EXIT_FAILURE);}}printf("Main: all threads completed.\n");return 0;
}
1.4.2 结束线程 pthread_exit
线程通过调用pthread_exit()函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。
void pthread_exit(void *retval);//retval用于存放线程结束的退出状态
1.4.3 线程等待 pthread_join
pthread_create调用成功以后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似与多进程编程中的waitpid。
- 作用:等待一个线程结束,并回收该线程的资源。
- 特点:这个函数会阻塞调用它的线程,直到被等待的线程终止。
- 使用场景:当你需要确认一个线程已经结束并获取它的退出状态时使用。
int pthread_join(pthread_t thread, void **retval);
/*示例:
假设 A 线程调用 pthread_join 试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。 该函数包含两个参数:pthread_t thread //thread是要等待结束的线程的标识
void **thread_return //指针thread_return指向的位置存放的是终止线程的返回状态。*/
//调用实例:
pthread_join(thread, NULL);
1.4.4 线程分离 pthread_detach
即主线程与子线程分离,子线程结束后,资源自动回收。
在POSIX线程库(pthread)中,
pthread_detach
函数用于将一个线程的状态设置为分离(detachedstate)状态。一个分离状态的线程在终止后,其资源会立即被回收,而不需要其他线程调用pthread_join
来等待它的结束。这样可以避免资源泄漏,同时也简化了线程管理。返回值:pthread_detach() 在调用成功完成之后返回零。其他任何返回值都表示出现了错误。如果检测到以下任一情况,pthread_detach()将失败并返回相应的值。
- 作用:将一个线程设置为分离状态。分离状态的线程在结束时,系统会自动回收它的资源。
- 特点:调用这个函数不会阻塞调用它的线程。分离状态的线程结束后,资源会自动回收,不需要通过
pthread_join
手动回收。- 使用场景:当你不关心线程的退出状态,只希望它结束时能自动回收资源时使用。
1.4.5 助理解线程等待函数和线程分离函数
通俗理解
pthread_join
:相当于你等你的朋友回家,等他回家了,你才能做其他事情。pthread_detach
:相当于你告诉朋友,不用告诉你他什么时候回家,他回家了自己开门,你继续做你的事情就好。
3、进程和线程的区别
3.1 区别
①内存空间:
进程:每个进程有自己独立的内存空间。
线程:同一进程中的线程共享内存空间。②资源开销:
进程:创建和切换进程的开销较大。
线程:创建和切换线程的开销较小。③通信方式:
进程:进程之间通信需要使用进程间通信机制(IPC),如管道、信号、共享内存等。
线程:线程之间直接共享内存,通信更方便,但需要注意同步和互斥。④独立性:
进程:一个进程崩溃不会直接影响其他进程。
线程:一个线程的异常可能导致整个进程崩溃。
3.2 举例
举一个生活中的对比助于理解
独立的公司(进程)和公司内部的部门(线程):
进程:每个公司(进程)有自己的办公室、设备和资源(内存和资源)。一个公司的倒闭(进程崩溃)不会直接影响到其他公司(进程)。
线程:每个公司 (进程) 内部可能有多个部门(线程),部门之间共享公司 (进程) 的资源(内存)。一个部门的错误操作(异常)可能会影响整个公司 (进程) 。
3.3 总结
①进程是操作系统分配资源和运行程序的基本单位,具有独立的内存空间和资源。
②线程是进程中的执行单元,共享进程的资源,但有独立的执行路径。
③进程和线程的主要区别在于资源的独立性和通信方式,进程之间更加独立,但通信复杂;线程之间通信方便,但需要注意同步和互斥。
④通过这些对比和生活中的类比,可以更好地理解进程和线程的概念及其应用。