Linux -- 多任务机制(任务、进程、线程)介绍

Linux -- 多任务机制(任务、进程、线程)介绍

  不管是在平平常常的工作中,还是在激动人心的面试中,都会或多或少、或深或浅的涉及到进程和线程相关的问题,作为常在河边走彳亍的人来说,再被问到关于进程和线程较为深入的问题的时候,虽然表面上稳如老狗,但是心里难免慌得一批。所以,为了不争馒头争口气,从今以后好好再学习一下关于进程和线程的东西,争取心里和面上都能够稳如老狗。

摘要

  多任务处理是指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务。 Linux就是一个支持多任务的操作系统,多任务操作系统使用某种调度策略支持多个任务并发执行。事实上。(单核)处理器在某一时刻只能执行一个任务。每个任务创建时被分配时间片(几十到上百毫秒),任务执行(占用CPU)时,时间片递减,操作系统会在当前任务的时间片用完时调度执行其他任务。由于任务会频繁地切换执行,因此给用户多个任务同时运行的感觉。

  多任务操作系统中通常有三个基本概念:任务进程线程。下面进行一一说明。

一、任务

  任务 是一个逻辑概念,指由一个软件完成的活动,或者是为实现个目的的一系列操作。通常一个任务是一个程序的一次运行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务是 进程 或者是 线程

  任务、进程和线程之间的关系如图3.1所示。

图3.1 任务、进程和线程之间的关系

二、进程和程序的区别

  程序 是一段静态的代码,是保存在非易失性存储器上的指令和数据的有序集合,没有任何执行的概念;

  进程 是一个动态的概念,它是程序的一次执行过程,包括了 动态创建、调度、执行和消亡 的整个过程,进程是一个 独立的可调度的任务,它是 程序执行和资源管理的最小单位

  进程 是一个抽象实体。当系统在执行某个程序时,分配和释放的各种资源;

  进程 是一个程序的一次执行的过程 。

  从操作系统的角度看,进程程序 执行时相关资源的总称。当进程结束时,所有资源被操系统自动回收。

三、进程

3.1、进程的基本概念

  进程 是指 一个具有独立功能的程序在某个数据集合上的一次动态执行过程

  它是操作系统 进行资源分配和调度的基本单元。一次任务的运行可以激活多个进程,这些进程相互合作来完成该任务的一个最终目标。

3.2、进程的主要特性

  1、并发性。指的是系统中多个进程可以同时并发执行,相互之间不受干扰;
  2、动态性。指的是进程都有完整的生命周期,而且在进程的生命周期内,进程的状态是不断变化的,另外,进程具有动态的地址空间(包括代码、数据和进程控制块等);
  3、交互性。指的是进程在执行过程中可能会与其他进程发生直接和间接的通信,如进程同步和进程互斥等,需要为此添加一定的进程处理机制;
  4、独立性。指的是进程是一个相对完整的资源分配和调度的基本单位,各个进程的地址空间是相互独立的,只有采用某些特定的通信机制才能实现进程之间的通信。

3.3、Linux 系统中进程的类型

  1、交互式进程。此类进程经常与用户进行交互,需要等待用户的输入(键盘和鼠标操作等)。当接收到用户的输入之后,这类进程能够立刻响应。

典型的交互式进程有shell命令进程、文本编辑器和图形应用程序运行等。

  2、批处理进程。此类进程不必与用户进行交互,因此通常在后台运行。因为这类进程通常不必很快地响应,因此往往不会优先调度。

典型的批处理进程是编译器的编译操作、数据库搜索引擎等。

  3、守护进程。这类进程一直在后台运行,和任何终端都不关联。通常系统启动时开始执行,系统关闭时才结束。很多系统进程(各种服务)都是以守护进程的形式存在。

3.4、Linux下的进程结构

  进程 包括程序的指令和数据,也包括程序计数器和处理器的所有寄存器以及存储临时数据的进程堆栈。
  因为Linux是一个多任务的操作系统,所以其他的进程必须等到操作系统将处理器使用权分配给自己之后才能运行。当正在运行的进程需要等待其他的系统资源时,Linux内核将取得处理器的控制权,按照某种调度算法将处理器分配给某个正在等待执行的进程。

  内核将所有的进程存放在双向链表(进程链表)中,链表的每一项都是 task_struct,称为 进程控制块的结构,该结构包含了与一个 进程相关的所有信息,在<include/Linux/sched.h>文件中定义。task_struct 内核结构比较大,它能完整地描述一个进程,如 进程的状态、进程的基本信息、进程标识符、内存相关信息、父进程相关信息、与进程相关的终端信息,当前工作目录,打开的文件信息、所接收的信号信息等。

  task_struct结构体定义如下所示。

struct task_struct {
	/*
	 * offsets of these are hardcoded elsewhere - touch with care
	 */
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	unsigned long flags;	/* per process flags, defined below */
	int sigpending;
	mm_segment_t addr_limit;	/* thread address space:
					 	0-0xBFFFFFFF for user-thead
						0-0xFFFFFFFF for kernel-thread
					 */
	struct exec_domain *exec_domain;
	volatile long need_resched;
	unsigned long ptrace;

	int lock_depth;		/* Lock depth */

/*
 * offset 32 begins here on 32-bit platforms. We keep
 * all fields in a single cacheline that are needed for
 * the goodness() loop in schedule().
 */
	long counter;
	long nice;
	unsigned long policy;
	struct mm_struct *mm;
	int has_cpu, processor;
	unsigned long cpus_allowed;
	/*
	 * (only the 'next' pointer fits into the cacheline, but
	 * that's just fine.)
	 */
	struct list_head run_list;
	unsigned long sleep_time;

	struct task_struct *next_task, *prev_task;
	struct mm_struct *active_mm;

/* task state */
	struct linux_binfmt *binfmt;
	int exit_code, exit_signal;
	int pdeath_signal;  /*  The signal sent when the parent dies  */
	/* ??? */
	unsigned long personality;
	int dumpable:1;
	int did_exec:1;
	pid_t pid;
	pid_t pgrp;
	pid_t tty_old_pgrp;
	pid_t session;
	pid_t tgid;
	/* boolean value for session group leader */
	int leader;
	/* 
	 * pointers to (original) parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p->father can be replaced with 
	 * p->p_pptr->pid)
	 */
	struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
	struct list_head thread_group;

	/* PID hash table linkage. */
	struct task_struct *pidhash_next;
	struct task_struct **pidhash_pprev;

	wait_queue_head_t wait_chldexit;	/* for wait4() */
	struct semaphore *vfork_sem;		/* for vfork() */
	unsigned long rt_priority;
	unsigned long it_real_value, it_prof_value, it_virt_value;
	unsigned long it_real_incr, it_prof_incr, it_virt_incr;
	struct timer_list real_timer;
	struct tms times;
	unsigned long start_time;
	long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
	unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
	int swappable:1;
/* process credentials */
	uid_t uid,euid,suid,fsuid;
	gid_t gid,egid,sgid,fsgid;
	int ngroups;
	gid_t	groups[NGROUPS];
	kernel_cap_t   cap_effective, cap_inheritable, cap_permitted;
	int keep_capabilities:1;
	struct user_struct *user;
/* limits */
	struct rlimit rlim[RLIM_NLIMITS];
	unsigned short used_math;
	char comm[16];
/* file system info */
	int link_count;
	struct tty_struct *tty; /* NULL if no tty */
	unsigned int locks; /* How many file locks are being held */
/* ipc stuff */
	struct sem_undo *semundo;
	struct sem_queue *semsleeping;
/* CPU-specific state of this task */
	struct thread_struct thread;
/* filesystem information */
	struct fs_struct *fs;
/* open file information */
	struct files_struct *files;
/* signal handlers */
	spinlock_t sigmask_lock;	/* Protects signal and blocked */
	struct signal_struct *sig;

	sigset_t blocked;
	struct sigpending pending;

	unsigned long sas_ss_sp;
	size_t sas_ss_size;
	int (*notifier)(void *priv);
	void *notifier_data;
	sigset_t *notifier_mask;
	
/* Thread group tracking */
   	u32 parent_exec_id;
   	u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty */
	spinlock_t alloc_lock;
};

  下面详细讲解 task_struct 结构中最为重要的两个域:state进程状态)和 pid进程标识符)。

volatile long state;
pid_t pid;

  其他想要详细了解 task_struct 结构体的其他成员,可以参考大佬文章:

  https://tanglinux.blog.csdn.net/article/details/7292563
  https://tanglinux.blog.csdn.net/article/details/7335187

3.5、Linux下的进程主要状态

  1、运行状态TASK_RUNNING)。进程当前正在运行,或者正在运行队列中等待调度。

  2、可中断的阻塞状态TASK_INTERRUPTIBLE)。进程处于阻塞(睡眠)状态,正在等待某些事件发生或能够占用某些共用资源。处在这种状态下的进程可以被信号中断。接收到信号或被显式地唤醒呼叫(如调用 wake_up 系列宏:wake_up、wake_up_interruptible 等)唤醒之后,进程将转变为 TASK_RUNNING 状态。

  3、不可中断的阻塞状态TASK_UNINTERRUPTIBLE)。此进程状态类似于可中断的阻塞状态(TASK_INTERRUPTIBLE),只是它不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在一些特定的情况下(进程必须等待,直到某些不能被中断的事件发生),这种状态是很有用的。只有在它所等待的事件发生时,进程才被显式地唤醒呼叫唤醒。

  4、暂停状态TASK_STOPPED)。进程的执行被暂停,当进程收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 等信号,就会进入暂停状态。

  5、僵死状态EXIT_ZOMBIE)。子进程运行结束,父进程未退出,并且未使用 wait 函数族(比如使用 wait、waitpid 等函数)等系统调用来回收子进程的退出状态。处在该状态下的子进程已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其父进程收集。

  6、消亡状态EXIT_DEAD)。这是最终状态,父进程用 wait 函数族回收之后,子进程彻底由系统删除,不可见。

  进程各个状态之间的转换关系如图3.2所示。

图3.2 进程各个状态之间的转换关系图

  内核可以使用 set_task_stateset_current_state 宏来改变指定进程的状态和当前执行进程的状态。
  set_task_state 宏定义如下所示。

#define __set_task_state(tsk, state_value)		\
	do { (tsk)->state = (state_value); } while (0)
#ifdef CONFIG_SMP
#define set_task_state(tsk, state_value)		\
	set_mb((tsk)->state, (state_value))
#else
#define set_task_state(tsk, state_value)		\
	__set_task_state((tsk), (state_value))
#endif

  set_current_state 宏定义如下所示。

#define __set_current_state(state_value)			\
	do { current->state = (state_value); } while (0)
#ifdef CONFIG_SMP
#define set_current_state(state_value)		\
	set_mb(current->state, (state_value))
#else
#define set_current_state(state_value)		\
	__set_current_state(state_value)
#endif

3.6、Linux下的进程的标识符

  Linux 内核通过唯一的进程标识符 PID 来标识每个进程,虽然是唯一的,但是z合格进程 ID 是可以复用的。但一个进程终止后,其进程 ID 就成为复用的候选者。

  此进程标识符定义在task_struct 结构体中pit_d pid 。系统中可以创建的进程数目有限制,可以通过指令 /proc/sys/kernel/pid_max 来确定进程上限。

  比如在 Ubunu 20.14 LTS 下此上限为 32768

$ cat /proc/sys/kernel/pid_max
32768

  当系统启动后,内核通常作为某一个进程的代表。一个指向 task_struct 的宏 current 用来记录正在运行的进程。 current 经常作为 进程描述符结构指针 的形式出现在内核代码中,例如,current->pid 表示 处理器正在执行的进程的PID,当系统需要查看所有的进程时,则调用 for_each_process() 宏,这将比系统搜索数组的速度要快得多。

  在 Linux 中可以通过函数 getpid()getppid() 来获取当前进程的进程号( PID )和父进程号(PPID )。

   getpid()getppid() 函数原型如下所示。

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

pid_t getpid(void);
pid_t getppid(void);

3.7、进程的创建、执行和终止

1、进程的创建和执行

  许多操作系统都提供产生进程的机制,也就是首先在新的地址空间里创建进程、读入可执行文件,最后再开始执行。

   Linux 中进程的创建很特别,它把上述步骤分解到两个单独的函数中执行: fork()exec函数族。
  首先,fork() 通过 复制当前进程创建一个子进程子进程父进程 的区别仅在于不同的 PIDPPID 和某些资源及统计量。 exec函数族负责 读取可执行文件 并将其 载入地址空间开始运行

  fork() 函数的原型如下所示:

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

  返回值说明:

若函数执行正确,则子进程返回为0,父进程返回子进程ID
若函数出错,则返回-1

  要注意的是,Linux 中的 fork() 使用的是 写时复制copy on write )的技术,也就是内核在创建进程时,其资源并没有立期被复制过来,而是被推迟到需要写入数据的时候才发生。在此之前只是以只读的方式共享父进程的资源。写时复制技术可以使Linux 拥有快速执行的能力。

2、进程的终止

  进程终止也需要做很多烦琐的收尾工作,系统必须保证进程所占用的资源回收,并通知父进程。

  Linux 首先把终止的进程设置为 僵死状态。这个时候,进程已经无法运行,它的存在只为父进程提供信息。父进程在某个时间调用wait函数族,回收子进程的退出状态,随后子进程占用的所有资源被释放。

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

  返回值说明:

如果函数成功,返回进程ID
如果函数出错,返回 -1

  进程的终止有5 种正常终止和3 种异常终止,下面分别进行简单的说明。
  5种正常终止的情况如下所示。

1、在 main 函数中执行 return 语句;
2、调用 exit 函数;
3、调用 _exit 函数或者 _Exit() 函数;
4、进程的最后一个线程在其启动例程中执行 return 语句;
5、进程的最后一个线程调用 pthread_exit 函数。

  3种异常的终止情况如下所示。

1、调用 abort
2、当进程接收到某些信号;
3、最后一个线程对“取消”请求作出响应。


  关于exit()、_exit()、return等详细说明,可以查看博文:
  https://songshuai0223.blog.csdn.net/article/details/120850739

  关于多进程的编程与实现,可以查看博文:
  https://songshuai0223.blog.csdn.net/article/details/120940142

3.8、进程的内存结构

  Linux 操作系统来用虚拟内存管理技术,使得每个进程都有独立的地址空间。该地址空间是大小为 4GB 的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但更安全(用户不能直接访问物理内存),而且用户程序可以使用比实际物理内存更大的地址空间。

  4GB 的进程地址空间会被升成两个部分:用户空间内核空间。用户地址空间是 0 - 3GB0 – 0xC0000000 ),内核地址空间占据 3 - 4GB 的空间位置,用户进程在通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程使用系统调用(代表用户进程在内核态执行)时才可以访问到内核空间。每当进程切换,用户空间就会跟者变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。每个进程的用户空间都是完全独立、互不相干的。

  进程的虚拟内存地址空间如图3.3所示。

图3.3 进程地址空间的分布图

  用户空间包括以下几个功能区域(通常也称之为 “段(segment”)。
  1、 只读段。具有只读属性,包含程序代码(.init、text )和只读数据(.rodata)。
  2、 数据段。存放的是全局变量和静态变量。其中初始化数据段(.data)存放 显式初始化的全局变量和静态变量;未初始化数据段,此段通常被称为BSS段(.bss),存放未进行显式初始化的全局变量和静态变量

  3、 。由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。
  4、 。存放动态分配的数据,一般由程序员动态分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
  5、 共享库的内存映射区域。这是 Linux 动态链接器和其他共享库代码的映射区域。

  因为在 Linux 系统中每一个进程都会有 /proc 文件系统下的与之对应的一个目录(比如,init 进程的相关信息存放在/proc/1 目录下),因此通过 proc 文件系统可以查看某个进程的地址空间的映射情况。例如,运行一个应用程序,如果他的进程号为13703,则输入 cat/proc/13703/maps 命令,可以查看该进程的内存映射情况。

$ cat /prac/13703/maps
/* 只读段:代码段、只读数据段 */
00400000-004f4000 r-xp 00000000 08:01 1594825                            /bin/bash
006f3000-006f4000 r--p 000f3000 08:01 1594825                            /bin/bash
/* 可读可写数据段 */
006f4000-006fd000 rw-p 000f4000 08:01 1594825                            /bin/bash
006fd000-00703000 rw-p 00000000 00:00 0 
/* 可读可读段  -- 堆 */
01d7f000-01f4c000 rw-p 00000000 00:00 0                                  [heap] 
/* 动态共享库 */
7f7717f40000-7f7717f4b000 r-xp 00000000 08:01 4467304                    /lib/x86_64-linux-gnu/libnss_files-2.23.so
/* 省略部分内容 */
... 
/* 可读可写数据区 */
7f7719629000-7f771962a000 rw-p 00000000 00:00 0
/* 堆栈 */ 
7ffdb411e000-7ffdb413f000 rw-p 00000000 00:00 0                          [stack]
7ffdb41da000-7ffdb41dd000 r--p 00000000 00:00 0                          [vvar]
7ffdb41dd000-7ffdb41df000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

四、线程

4.1、线程概述

  众所周知,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段、代码段和堆栈段,这就造成了进程在进行切换时操作系统的开销比较大。

  所以,为了提高效率, 操作系统又引入了 线程 概念,也称为 轻量级进程。线程可以对进程的内存空间和资源进行访问,并与同一进程中的其他线程共享。因此, 线程的上下文切换的开销比进程小得多。

  另外,一个进程可以拥有多个线程,其中每个线程共享该进程所拥有的资源。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响。所以,多线程中同步是非常关键的问题。

  进程和线程之间的关系如图3.4所示。

图3.4 进程和线程之间的关系

4.2、线程标识

  每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID、一组寄存器、栈、调度优先级和策略、信号屏蔽字、errno标量、线程私有数据等。一个进程的所有信息对该进程的所有线程是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈、文件描述符。

  线程也有一个线程IDLinux环境下,线程ID是用无符号长整形来表示的),与进程ID不同的是,线程ID只有在它所属的进程上下文才有意义

  线程可以通过调用函数 pthread_self() 来获取自身的线程ID

#include <pthread.h>
pthread_t pthread_self(void);

返回值:调用线程的线程ID

4.3、线程的创建和终止

1、线程的创建

  新增线程可以通过调用函数 pthread_create() 来实现。

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

2、线程的终止

  如果进程中任意线程调用了exit()、_Exit()、_exit(),那么整个进程就会终止,与此类似,如果默认的动作是终止进程,那么发送到线程的信号就会终止整个进程。

  单个线程可以有3种方式退出,因此在不终止整个进程的情况下,停止它的控制流。

1、线程可以简单地从启动例程中返回,返回值线程的退出码;
2、线程可以被同一进程中的其他线程取消;
3、线程调用pthread_exit()函数。

  线程可以通过调用 pthread_cancel() 函数来取消同一进程中的其他线程。

#include <pthread.h>
int pthread_cancel(pthread_t thread);

  线程可以通过调用 pthread_exit() 函数来取消线程。

#include <pthread.h>
void pthread_exit(void *retval);

  关于多线程的编程与实现,可以查看博文:
  https://songshuai0223.blog.csdn.net/article/details/120830985

五、进程和线程的区别

  1、基本区别:进程是资源分配最小单位,线程是程序执行的最小单位;
  2、空间关系:同一进程的各个线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的;
  3、所属关系:线程是进程的一部分,进程中可以包含很多个线程(进程中至少有一个线程,称为主线程),线程也称为轻量级进程;
  4、影响关系:进程相互独立(独立的地址空间),所以一个进程崩溃后,一般不会对其他进程产生影响,但是一个线程(共享地址空间)崩溃整个进程都异常崩溃;
  5、通信关系:同一个进程下,线程共享全局变量,静态变量等数据,所以线程之间的通讯简单,但是存在同步与互斥的难点,进程之间的通信需要以通信的方式(IPC)进行;
  6、开销方面:每个进程都有独立的代码和数据空间(程序上下文),进程之间切换开销大;线程之间共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小;
  7、资源方面:进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
  8、 环境关系:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行;
  9、执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,进程和线程都可以并发执行。

  
  好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然 关注一波 那就更好了,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。

上一篇:本文
下一篇:Linux – 多进程的编程之 - 基础实现、孤儿进程

  • 25
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

青椒*^_^*凤爪爪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值