Bootstrap

C语言进程——基础

什么是进程

什么是程序
一组可以被计算机直接识别的 有序 指令 的集合。
通俗讲:C语言编译后生成的可执行文件就是一个程序。

那么程序是静态还是动态的?
程序是可以被存储在磁盘上的,所以程序是静态的。

那什么是进程

  1. 进程是程序的执行过程,是动态的,随着程序的使用被创建,随着程序的结束而消亡。
  2. 可以说进程是一个独立的可调度的任务。
  3. 进程是系统调度的独立任务。
  4. 进程是程序执行的独立任务。
  5. 进程是资源(内存资源)管理的最小任务。

一个程序可以只有一个进程,此时正在运行的这个程序也叫进程。
一个程序也可以有多个进程,此时正在运行的这个程序有多个进程动态执行。
所以说进程可以是程序,但程序不一定是进程。

注意:每一个程序运行时,操作系统分配给进程的 是虚拟内存,意味着每一个进程所使用的空间都是虚拟内存, 虚拟内存会被单元管理模块(MMU)映射到物理内存上,如何映射是操作系统关心的事情,程序开发者不用关心。

时间片
进程有多个,而CPU只有一个,假设该CPU是单核的,那么在某一时刻CPU只能处理一个进程,但是不能一直去处理这个进程,得多个进程之间轮流处理,给用户感觉这些进程在同时进行,而CPU处理一个进程的时间段即时间片。时间片是约定好CPU处理一个进程的时间段。

进程的类型

  1. 交互进程:完成人机交互的进程,比如shell
  2. 批处理进程:比如gcc的四步流程
  3. 守护进程:开机自启动,关机自动关闭(后台运行)

进程的状态

  1. 就绪状态:具备运行条件,等待处理器运行的进程。
    当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
  2. 运行状态:处理器正在运行的进程。
  3. 等待状态:又称阻塞态或睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。
    也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。
  4. 死亡状态:运行结束的进程。

进程的模式

  1. 终端:内核发送的信号。
  2. 系统调用:调用操作系统提供给用户来访问硬件的一组接口。

在这里插入图片描述

进程三态模型
运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
等待态→就绪态:资源得到满足;如外设传输结束;人工干预完成。
运行态→就绪态:运行时间片到;出现有更高优先权进程。
就绪态—→运行态:CPU 空闲时选择一个就绪进程。
在这里插入图片描述

进程五态模型
在这里插入图片描述

孤儿进程
指父进程先于子进程退出,此时子进程称为孤儿进程。但是该进程会被pid为1的init进程收养。

僵尸进程
指子进程先于父进程退出并且没有被父进程回收子进程的资源。此时子进程就会变成僵尸进程。僵尸进程会造成浪费空间、资源泄露等问题。

进程的相关系统调用

在这里插入图片描述

创建进程

  1. 每个进程都由父进程创建。
  2. 通过系统调用函数 fork() 实现进程创建。

fork()

  1. 头文件:<sys/types.h> <unistd.h>
  2. 函数原型:pid_t fork();
  3. 返回值:PID,进程ID号。返回 0 表示子进程,返回-1失败,返回大于0的整数表示创建进程的PID。
  4. 可以通过getpid()来获取当前运行的进程ID,通过getppid()获取当前进程的父进程ID。

wait()

  1. 头文件:<sys/wait.h><sys/types.h>
  2. 函数原型:pid_t wait(int* status) status为空时表示忽略子进程退出时的状态,不为空表示保存子进程退出时的状态。
  3. 返回值:成功返回子进程的PID,失败返回-1
  4. 使进程进入阻塞状态。
  5. 直到任意子进程结束或者该进程接收到信号为止。
  6. 如果该进程没有子进程,或子进程已经结束。wait()会立即返回。
  7. 此函数时进程阻塞时父进程什么也不干。
  8. 该函数可以获取子进程终止使的退出状态。

waitpid()

  1. 函数原型:pid_t waitpid(pid_t pid, int *status, int options)

  2. 入参:pid

    1. pid 传-1时 等待任意子进程与wait功能一样。
    2. pid 传0时 等待其组ID等于调用进程的组ID的任意子进程。
    3. pid 传 小于-1时 等待其组ID等于PID的绝对值的任意子进程。
  3. 入参:status 同wait

    1. status 通过WIFEXITED宏来测验 子进程正常退出返回true,否则返回false
      在这里插入图片描述

    2. status 通过WEXITSTATUS宏 来查看退出状态值。
      在这里插入图片描述

    3. return exit() _exit() 在WIFEXITED看来都算正常退出

  4. 入参:options

    1. 传0 同wait 阻塞父进程
    2. 传WNOHANG:若由PID指定的子进程并不立即可用,则waitpid不会被阻塞,此时返回值为0,子进程结束时返回子进程PID
  5. 返回值:正常返回结束的子进程PID,-1失败,

  6. 功能与wait类似。

  7. 可以指定等待某个子进程以及等待方式(阻塞或非阻塞)

wait和waitpid都可以实现对子进程资源的回收

exit(int status)
status:退出状态。
使进程终止,并清空缓冲区。

_exit(int status)
使进程终止,但是不会清空缓冲区。

在这里插入图片描述

Exec函数族

以exec开头的一系列函数
该族函数提供了在一个进程中执行新的进程
通过fork开启的子进程中拥有与父进程相同的代码,但是开辟了新的空间,这么做实际意义不大。所以exec族函数可以对fork创建的子进程进行代码替换,只保留PID不变,这就实现了在一个进程中产生了新的进程。
在这里插入图片描述

参数意义
l(list)参数地址列表,以空指针结尾
v(vector)存有各参数地址的指针数组的地址
p(path)按 PATH 环境变量指定的目录搜索可执行文件
e(environment)存有环境变量字符串地址的指针数组的地址

守护进程

  1. 运行在后台的进程,与终端没有任何关系。
  2. 开机自启动,关机自关闭。

前台进程
和终端有关系的进程

后台进程
与终端脱离关系。

变成后台进程的步骤

  1. 首先变成孤儿进程。
  2. 让自己成为新的进程组组长。
  3. 让自己成为新的会话组组长。
  4. 使控制终端tty变成 ‘?’ 才能完全脱离终端。
    在这里插入图片描述

创建守护进程的步骤

  1. 创建子进程父进程退出。(为了让子进程先被init收养)

  2. 创建新的会话组。(通过setsid()函数)让自己成为新的会话组组长。
    在这里插入图片描述
    在这里插入图片描述

  3. 此时守护进程已经创建,但是还需要优化。再使用chdir()函数修改守护进程的工作路径。
    在这里插入图片描述

  4. 重设文件掩码。将文件掩码设置为0可以增加守护进程的灵活性。 在这里插入图片描述

  5. 关闭父进程继承过来的文件描述符。因为守护进程用不到这些资源,会造成资源浪费。
    在这里插入图片描述

  6. getdtablesize()返回一个进程可以打开的最大文件数

  7. 再到/etc/rc.local 文件中exit 0之前 将这个守护进程的绝对路径写在这里。开机自启动。
    在这里插入图片描述

线程

多个进程中通过轮流使用CPU来完成自己的任务,如果多个进程的操作都一模一样那么CPU的开销就会很大,因为进程的地址都是私有的,如果CPU对相同的操作只执行一次,后面再遇到直接去获取即可,这样大大降低了CPU的开销,如此就引出了线程。
在这里插入图片描述
所谓线程就是一个轻量级的进程。
在同一进程中可以创建多个线程共享这个进程的地址空间。
线程使操作系统可调度的最小单位。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

对于操作系统而言,线程与进程没有区别。

线程的基本操作

  1. 创建线程
    在这里插入图片描述
  2. 删除线程
  3. 控制线程

线程相关函数

  1. pthread_join(pthread_t tid, void ** retval) : 等待子线程结束后回收资源。

    1. 参数1:线程号。
    2. 参数2:线程函数的返回结果。在这里插入图片描述
  2. pthread_exit(void*); 线程函数中的返回函数,跟return类似。
    在这里插入图片描述

  3. pthread_detach(pthread_t id); // 主线程中调用线程分离,子线程中调用将子线程设置为游离态,主线程不再阻塞式等待子线程完成后才进行自己的工作,该函数会将子线程的回收工作交给内核去做。

  4. pthread_self(); // 获取当前线程的ID

线程的状态

  1. 新建:新创建的一个线程。
  2. 就绪:准备运行的线程。
  3. 运行:正在运行的线程。
  4. 等待:也叫阻塞。
  5. 死亡:运行结束的线程。

多线程

同步与互斥
在这里插入图片描述

  1. 信号量在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  2. P / V操作
    在这里插入图片描述

  3. 互斥锁在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

pthread_mutex_destroy(pthread_mutex_t *mutex); 销毁锁。
pthread_mutex_lock(pthread_mutext_t *mutex); 上锁。
pthread_mutex_unlock(pthread_mutext_t *mutex); 解锁。

传统的进程间的通信

  1. 无名管道

    1. 管道的创建是放在内存的内核区中,所以是不能很直观的看到管道的。

    2. linux中管道也是文件。(管道文件)所以管道成功创建后会返回两个文件描述符,分别是读端和写端。(fd[0]读和fd[1]写)

    3. 一般来说两个进程一个发一个收,那么一端关闭fd[0]读操作,另一端关闭fd[1]写操作。

    4. 因为管道在内核区,用户对其操作只能用系统调用write和read操作,而不能用fwrite和fread

    5. 管道只能实现具有血缘关系的进程才能进行通信,否则会出现我创建的管道你找不到的情况。

    6. 管道的创建与关闭在这里插入图片描述在这里插入图片描述

  2. 有名管道(命名管道)

    1. 无名管道必须是有血缘关系的进程之间通信,但是实际情况并不是这样,现实中大多需要没有任何关系的进程间通信。这时就需要使用有名管道进行通信。

    2. 那么怎么能使不同进程间都找到这个管道呢?这时就有了管道文件,不同进程间可以通过对这个文件的读写来实现通信。

    3. 实际上这个管道文件存在于文件系统,这个文件的作用只是为了让没有血缘关系的两个进程能够找到存储在内核区中的同一个管道。读写的操作实际上还是通过内核区的管道。

    4. 把这个管道文件看作是内核区中的管道的名字以此来找到同一个内核区的管道,所以称其为有名管道。

    5. 在这里插入图片描述

    6. 特点:

      1. 可以实现任意两个进程之间的通信。
      2. 通信时双方通过一个管道文件进行操作,但实际上通过管道文件标识内核区管道来进行读写操作。这个文件的大小始终为0
      3. 管道文件是存在于文件系统中的。
    7. 在这里插入图片描述

    8. 在这里插入图片描述在这里插入图片描述

  3. 信号量

    1. 无名信号量:解决线程之间的同步与互斥。
      1. 无名信号量的使用案例:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

// 临界区:盘子
int plate = 4; // 最多只能放4个水果
int orange = 0; // 盘子中橘子的数量
int apple = 0; // 盘中苹果的数量

// 消费者最大值
int son_max = 5; // 儿子最多吃5个橘子
int girl_max = 5;// 女儿最多吃5个苹果

// 创建互斥锁,同一时刻只能有一个人放水果或拿水果
pthread_mutex_t production;
pthread_mutex_t consumer;

// 信号量
sem_t s_dad, s_mom, s_son, s_girl;

void* thread_dad(void *sp)
{
	while(1)
	{
		pthread_mutex_lock(&production);// 放水果占有盘子
		sem_wait(&s_dad);
		if(0 == son_max){
			printf("儿子吃饱了\n");
			sem_destroy(&s_son);
			pthread_mutex_unlock(&production);
			pthread_exit(NULL);
		}

		if(plate > 0){
			printf("爸爸放了一个橘子\n");
			--plate; // 盘子容量-1
			++orange;// 橘子个数+1
		}
		else{
			printf("盘满了\n");
		}
		sem_post(&s_son); // 告诉儿子盘中有水果了
		pthread_mutex_unlock(&production);// 放完水果释放盘子
		sleep(1);
	}
	pthread_exit(NULL);
}

void* thread_mom(void *sp)
{
	while(1)
	{
		pthread_mutex_lock(&production);// 占用盘子放水果
		sem_wait(&s_mom);
		if(0 == girl_max){
			printf("女儿吃饱了\n");
			sem_destroy(&s_mom);
			pthread_mutex_unlock(&production);
			pthread_exit(NULL);
		}

		if(plate > 0){
			printf("妈妈放了一个苹果\n");
			--plate;// 盘子容量-1
			++apple;// 苹果个数+1
		}
		else{
			printf("盘满了\n");
		}
		sem_post(&s_girl); // 告诉女儿盘中有水果了
		pthread_mutex_unlock(&production);// 释放盘子
		sleep(1);
	}
	pthread_exit(NULL);
}

void* thread_son(void *sp)
{
	while(1)
	{
		pthread_mutex_lock(&consumer);// 占用盘子拿橘子
		sem_wait(&s_son);

		if(plate == 4){
			printf("儿子说盘子空了\n");
			sem_post(&s_dad);// 通知爸爸放橘子
			pthread_mutex_unlock(&consumer);
			sleep(1);
			continue;
		}

		if(orange > 0){
			printf("儿子吃掉了一个橘子\n");
			++plate;// 盘子容量+1
			--orange;// 橘子数量-1
			--son_max;//肚量-1
			if(son_max == 0){
				printf("吃饱了\n");
				pthread_mutex_unlock(&consumer);
				sem_post(&s_dad);// 告诉爸爸不要放橘子了
				sem_destroy(&s_son);// 不吃了
				pthread_exit(NULL);
			}
			sleep(1);
		}
		else{
			printf("没有橘子了\n");
			sleep(1);
		}
		sem_post(&s_dad);// 通知爸爸做橘子
		pthread_mutex_unlock(&consumer);// 释放拿的权限
		sleep(1);

	}
	pthread_exit(NULL);
}

void* thread_girl(void *sp)
{
	while(1)
	{
		pthread_mutex_lock(&consumer);// 占用盘子准备拿水果
		sem_wait(&s_girl);

		if(plate == 4){
			printf("女儿说盘子空了\n");
			sem_post(&s_mom);
			pthread_mutex_unlock(&consumer);
			sleep(1);
			continue;
		}
		if(apple > 0){
			printf("女儿吃掉了一个苹果\n");
			++plate;// 盘子容量+1
			--apple;// 苹果数量-1
			--girl_max;// 肚量-1
			if(girl_max == 0){
				printf("吃饱了\n");
				pthread_mutex_unlock(&consumer);
				sem_post(&s_mom);// 告诉妈妈不要放苹果了
				sem_destroy(&s_girl);// 不吃了	
				pthread_exit(NULL);
			}
			sleep(1);
		}
		else{
			printf("盘中没有苹果了\n");
			sleep(1);
		}
		sem_post(&s_mom);// 通知妈妈放苹果
		pthread_mutex_unlock(&consumer);
		sleep(1);
	}
	pthread_exit(NULL);
}


int main()
{
	pthread_mutex_init(&production, NULL);
	pthread_mutex_init(&consumer, NULL);
	sem_init(&s_son, 0, 0);
	sem_init(&s_girl, 0, 0);
	sem_init(&s_dad, 0, 1);
	sem_init(&s_mom, 0, 1);

	// 生产者:爸爸往盘中放橘子,妈妈放苹果
	pthread_t dad = 1, mom = 2;
	pthread_create(&dad, NULL, thread_dad, NULL);// 爸爸
	pthread_create(&mom, NULL, thread_mom, NULL);// 妈妈


	// 消费者:儿子吃橘子,女儿吃苹果
	pthread_t son = 3, girl = 4;
	pthread_create(&son, NULL, thread_son, NULL);// 儿子
	pthread_create(&girl, NULL, thread_girl, NULL);// 女儿

	pthread_join(dad, NULL);
	pthread_join(mom, NULL);
	pthread_join(son, NULL);
	pthread_join(girl, NULL);

	pthread_mutex_destroy(&production);
	pthread_mutex_destroy(&consumer);

	return 0;
}

  1. 有名信号量:解决进程之间的同步与互斥。
    1. 打开双方都认识的有名信号量文件(双方都可以创建)
    2. P操作:sem_wait()
    3. V操作:sem_post()
    4. 关闭有名信号量:sem_close()
    5. 删除创建的有名信号量:sem_unlink()
    6. 有名信号量创建后在 /dev/shm 下

基于System V IPC对象的进程间通信

ipcs:可以查看linux系统中所有共享内存,消息队列,信号灯集
ipcrm -m ID号:删除指定ID的通信机制。
在这里插入图片描述
注意:基于System V IPC对象的三种通信方式都是存储在内核中的。
在这里插入图片描述

  1. 共享内存
    其高效是因为,内核区内存的物理地址通过映射 到用户区的虚拟地址,用户通过该地址直接完成读写操作。

    1. 是一种最为高效的进程间通信方式,进程可以直接读写共享内存,而不需要任何数据的拷贝。
    2. 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。
    3. 进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高效率。
    4. 由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。
    5. 共享内存中的数据是一直存在的除非删除这个共享内存,不像管道读完后管道中就没有数据了。

    共享内存的使用
    1. 创建/打开共享内存。ftok()函数产生key值,shmget()函数通过key值创建共享内存
    在这里插入图片描述
    在这里插入图片描述

  2. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问。
    在这里插入图片描述

  3. 撤销共享内存映射。(也就是分离)
    在这里插入图片描述

  4. 删除共享内存对象。
    在这里插入图片描述

  5. 消息队列

    1. 特点:
      1. 存储在内核中。
      2. 可以按照类型读取消息。
    2. 流程
      1. 产生key值。 ftok()函数

      2. 创建消息队列的通道。
        在这里插入图片描述

      3. 添加消息。在这里插入图片描述

      4. 读取消息。在这里插入图片描述

      5. 删除消息。
        在这里插入图片描述

信号
linux中共有64个信号,通过kill -l 查看
在这里插入图片描述

  1. 信号的处理方式:
    1. 终止。
    2. 忽略。
    3. 捕捉。
  2. 信号安装函数(系统默认处理方式是终止)在这里插入图片描述
  3. 捕捉函数:给指定进程发送信号,pid传0给自己发在这里插入图片描述
    2. raise只能给自己发信号在这里插入图片描述
  4. 挂起函数(alarm(秒数) 时间到之后如果程序还在执行,就发送SIGALARM信号。如果程序在设置的秒数内已经结束则程序正常结束)在这里插入图片描述
    在这里插入图片描述
;