Linux并发程序设计_进程01

进程概念

程序
存放在磁盘上的指令和数据的有序集合(文件)
静态的

进程
执行一个程序所分配的资源的总称
进程是程序的一次执行过程
动态的,包括创建、调度、执行和消亡

程序和进程的区别:
程序是一段静态的代码,是保存在非易失性存储器上的指令和数据的有序集合,没有任何执行的概念;
而进程是一个动态的概念,它是程序一次的执行过程,包括了动态创建、调度、执行和消亡的整个过程,
它是程序执行和管理的最小单位

转载了这篇博主,介绍的很详细,感谢他的文章分享。

https://blog.csdn.net/fuqin163/article/details/1546919

  1. 进程是程序的一次运行活动,属于一种动态的概念。 程序是一组有序的静态指令,是一种静 态 的 概 念。 但 是, 进 程 离 开 了程 序 也 就 没 有 了 存 在 的 意 义。 因 此, 我 们 可 以 这 样 说: 进 程 是 执 行 程 序 的 动 态 过 程, 而 程 序 是 进程 运 行 的 静 态 文 本。 如 果 我 们 把 一 部 动 画 片 的 电 影 拷 贝 比 拟 成 一 个 程 序, 那 么 这 部 动 画 片 的 一次 放 映 过 程 就 可 比 为 一 个 进 程。

  2. 一 个 进 程 可 以 执 行 一 个 或 多个 程 序。 例 如: 一 个 进 程 进 行C 源 程 序 编 译 时,它 要 执 行 前 处 理、 词 法 语 法 分 析、 代 码 生 成 和 优 化 等 几 个 程 序。 反 之, 同 一 程 序 也 可 能 由 多 个 进程 同 时 执 行, 例 如: 上 述C 编 译 程 序 可 能 同 时 被 几 个 程 序 执 行, 它 们对 相 同 或 不 同 的 源 程 序 分 别 进 行 编 译, 各 自 产 生 目 标 程 序。 我 们 再 次 以 动 画 片 及 其 放 映 活 动 为例, 一 次 电 影 放 映 活 动 可 以 连 续 放 映 几 部 动 画 片, 这 相 当 于 一 个 进 程 可 以 执 行 几 个 程 序。 反 之,一 部 动 画 片 可 以 同 时 在 若 干 家 电 影 院 中 放 映, 这 相 当 于 多 个 进 程 可 以 执 行 几 个 同 一 程 序。 不 过要 注 意 的 是, 几 家 电 影 院 放 映 同 一 部 电 影, 如 果 使 用 的 是 同 一 份 拷 贝, 那 么 实 际 上 是 交 叉 进 行 的。但 在 多 处 理 机 情 况 下, 几 个 进 程 却 完 全 可 以 同 时 使 用 一 个 程 序 副 本。

  3. 程 序 可 以 作 为 一 种 软 件 资 源长 期 保 持 着, 而 进 程 则 是 一 次 执 行 过 程, 它 是 暂时 的, 是 动 态 地 产 生 和 终 止 的。 这 相 当 于 电 影 拷 贝 可 以 长 期 保 存, 而 一 次 放 映 活 动 却 只延 续1~2 小 时。

进 程 需 要 使 用 一 种 机 构才 能 执 行 程 序, 这 种 机 构 称 之 为 处 理 机(Processor)。 处 理 机 执 行 指令, 根 据 指 令 的 性 质, 处 理 机 可 以 单 独 用 硬 件 或 软、 硬 件 结 合 起 来 构 成。 如 果 指 令 是 机 器 指 令, 那么 处 理 机 就 是 我 们 一 般 所 说 的 中 央 处 理 机(CPU)。

进程包含的内容

在这里插入图片描述
进程控制块、CPU寄存器值、堆栈

进程控制块

转载这篇博主的文章,感谢他的分享

https://blog.csdn.net/mxrrr_sunshine/article/details/79538827

操作系统对这些进程只做两件事情:“描述”+“组织”。
描述:将进程描述出来,就需要描述出来当前进程的各种信息,我们的操作系统将这些各种信息放在一个结构体中,这个结构体就称为进程控制块。
组织:用我们所学过的数据结构的各种各样的数据结构知识将这些结构体组织起来,例如:双链表结构,索引表结构。
索引表:同一状态的进程归入一个索引表,多个状态对应多个不同的索引表。
在这里插入图片描述
链表:
同一状态的进程其PCB成一链表,多个状态对应多个不同的链表。
在这里插入图片描述
那么 进程控制块中具体有什么呢?

  1. 标识符:与进程相关的唯一标识符,用来区分其他进程。
  2. 状态:进程有不同的状态,例如运行,就绪,阻塞等。
  3. 优先级:为了给处理机调度提供支持,优先级用来区分各种进程的先后执行顺序
  4. 程序计数器:程序中即将执行的下一条指令的地址
  5. 内存指针:包括程序代码的进程相关数据的指针,以及与其他进程贡献那个内存块的指针。
  6. 上下文:进程是需要切换的,切换进程时需要保留进程的上下文信息,以防进程在切换时丢失现场数据。
  7. i/o状态信息:包括显示i/o请求,分配给进程的i/o设备和被进程使用的文件列表等。
  8. 记账信息:包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

进程控制块是操作系统为支持多进程并提供多重处理技术的关键。 进程有很多,但cpu只有一个,所有有时进程是需要切换的,PCB的作用可以使进程在恢复后,像从未中断过一样。
所以在许多操作系统书中经常会看到:PCB是进程存在的唯一标志

进程类型

交互进程:交互式进程:这类进程经常与用户进行交互,需要等待用户的输入(键盘和鼠标操作等)。当接收到
用户的输入之后,这类进程能够立刻响应。典型的交互式进程有 shell 命令进程、文本编辑器和图形应用程序
运行等。

批处理进程:这类进程不必与用户进行交互,因此通常在后台运行。因为这类进程通常不必很快
地响应,因此往往不会优先调度。典型的批处理进程是编译器的编译操作、数据库搜索引擎等

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

进程状态

运行态:进程正在运行,或者准备运行

等待态:进程在等待一个事件的发生或某种系统资源
可中断
不可中断

停止态:进程被中止,收到信号后可继续运行

死亡态:已终止的进程,但pcb没有被释放

进程状态图
在这里插入图片描述

内核将所有进程存放在双向循环链表(进程链表)中,链表的每一项都是 task_struct,称为进程控
制块的结构。该结构包含了与一个进程相关的所有信息,在<include/Linux/sched.h>文件中定义。task_struct 内核结构比较大,它能完整地描述一个进程,如进程的状态、进程的基本信息、进程标识符、内存相关信息、父进程相关信息、与进程相关的终端信息、当前工作目录、打开的文件信息、所接收的信号信息等。
(1)进程状态。
Linux 中的进程有以下几种主要状态。
① 运行状态(TASK_RUNNING):进程当前正在运行,或者正在运行队列中等待调度。
② 可中断的阻塞状态(TASK_INTERRUPTIBLE):进程处于阻塞(睡眠)状态,正在等待某些事件发生
或能够占用某些资源。处在这种状态下的进程可以被信号中断。接收到信号或被显式地唤醒呼叫(如调用
wake_up 系列宏:wake_up、wake_up_interruptible 等)唤醒之后,进程将转变为 TASK_RUNNING 状态。
③ 不可中断的阻塞状态(TASK_UNINTERRUPTIBLE):此进程状态类似于可中断的阻塞状态
(TASK_INTERRUPTIBLE),只是它不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在
一些特定的情况下(进程必须等待,直到某些不能被中断的事件发生),这种状态是很有用的。只有在它
所等待的事件发生时,进程才被显式地唤醒呼叫唤醒。
④ 暂停状态(TASK_STOPPED):进程的执行被暂停,当进程收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU
等信号,就会进入暂停状态。
⑤ 僵死状态(EXIT_ZOMBIE):子进程运行结束,父进程尚未使用 wait 函数族(如使用 waitpid()函
数)等系统调用来回收退出状态。处在该状态下的子进程已经放弃了几乎所有的内存空间,没有任何可执
行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其父进程收集。
⑥ 消亡状态(EXIT_DEAD):这是最终状态,父进程调用 wait 函数族回收之后,子进程彻底由系统删
除。

(2)进程标识符。
Linux 内核通过唯一的进程标识符 PID 来标识每个进程。PID 存放在 task_struct 的 pid 字段中。系统中
可以创建的进程数目有限制,读者可以查看/proc/sys/kernel/pid_max 来确定上限。
当系统启动后,内核通常作为某一个进程的代表。一个指向 task_struct 的宏 current 用来记录正
在运行的进程。current 经常作为进程描述符结构指针的形式出现在内核代码中,例如,current→pid
表示处理器正在执行的进程的 PID。当系统需要查看所有的进程时,则调用 for_each_process()宏,这将
比系统搜索数组的速度要快得多。
在 Linux 中获得当前进程的进程号(PID)和父进程号(PPID)的系统调用函数分别为 getpid()和
getppid()。

(3)进程的内存结构
用户空间包括以下几个功能区域。
(1)只读段:包含程序代码(.init 和.text)和只读数据(.rodata)。
(2)数据段:存放的是全局变量和静态变量。 其中可读可写读数据段(.data)存放已初始化的全
局变量和静态变量,BSS 数据段(.bss)存放未初始化的全局变量和静态变量。
(3)栈:由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。
(4)堆:存放动态分配的数据,一般由程序员动态分配和释放,若程序员不释放,程序结束时可能
由操作系统回收。
(5)共享库的内存映射区域:这是 Linux 动态链接器和其他共享库代码的映射区域。
在这里插入图片描述

查看进程信息

ps 查看系统进程快照
在这里插入图片描述
在这里插入图片描述
top 查看进程动态信息
在这里插入图片描述
/proc 查看进程详细信息

在这里插入图片描述
nice 按用户指定的优先级运行进程

linux@linux:~/Desktop/Homework/L1_LinuxC$ nice -n 5 ./test 

在这里插入图片描述
renice 改变正在运行进程的优先级
在这里插入图片描述
jobs 查看后台进程
在这里插入图片描述
bg 将挂起的进程在后台运行
在这里插入图片描述
fg 把后台运行的进程放到前台运行
在这里插入图片描述

进程创建 – fork

转载这篇博主的文章,感谢他的分享,写的很棒

https://www.cnblogs.com/dongguolei/p/8086346.html

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

在这里插入图片描述

功能 创建一个新的进程
头文件 #include <unistd.h>
原型 pid_t fork(void);
返回值 成功 0 或者大于 0 的正整数
失败 -1
备注 该函数执行成功之后,将会产生一个新的子进程,在新的子进程中其返回值为 0,在原来的父进程中其返回值为大于 0 的正整数,该正整数就是子进程的 PID

在这里插入图片描述
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

int main(int argc, const char *argv[])
{
	pid_t fpid; //fpid表示fork函数返回的值
	int count = 0;
	
	
	fpid = fork();

	if(fpid < 0)
	{
		puts(" Error in fork!");

	}
	else if(fpid == 0)
	{
		printf(" \n I am child process, my process id is %d\n",getpid());
		count++;
	}
	else
	{
		printf(" \n I am parents process, my process id is %d\n",getpid());
		count++;
	}
	
	printf(" sum = %d\n ",count);

	return 0;
}

运行结果:
在这里插入图片描述
在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;

在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.

fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
fork执行完毕后,出现两个进程,
在这里插入图片描述
有人说两个进程的内容完全一样啊,怎么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量啊。
执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。
还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。

父子进程

子进程继承了父进程的内容
父子进程有独立的地址空间,互不影响
若父进程先结束:
子进程成为孤儿进程,被init进程收养
子进程变成后台进程
若子进程先结束:
父进程如果没有及时回收,子进程变成僵尸进程

进程结束 – exit/_exit

#include <stdlib.h>
#include <unistd.h>
void exit(int status);
void _exit(int status);
在这里插入图片描述

功能 退出本进程
头文件 #include <unistd.h>
#include <stdlib.h>
原型 void exit(int status);
void _exit(int status);
参数 status 子进程的退出值
返回值 不返回
备注 1,如果子进程正常退出,则 status 一般为 0。2,如果子进程异常退出,则 statuc 一般为非 0。3,exit( )退出时,会自动冲洗(flush)标准 IO 总残留的数据到内核,如果进程注册了“退出处理函数”还会自动执行这些函数。而_exit( )会直接退出。
int main(int argc, const char *argv[])
{
	puts(" 1-this process will exit ");
	exit(0);
	puts(" 2-never be displayed ");
	return 0;
}

在这里插入图片描述

int main(int argc, const char *argv[])
{
	puts(" 1-this process will exit ");
	puts(" 2-will displayed ");
	exit(0);
	return 0;
}

在这里插入图片描述

exit()和_exit()的区别
转载这篇博主的文章,感谢他的分享

https://blog.csdn.net/shimadear/article/details/80380905

exit函数:定义在C标准库stdlib.h中;当我们调用exit(0)时,表示正常退出当前进程,当我们调用eixt(1)时表示非正常退出当前进程。
_exit函数:定义在unistd.h中;直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit() 函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。
exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是"清理I/O缓冲"。
用一张图来表示:

在这里插入图片描述

进程 – exec函数族

(1)exec 函数族说明。
fork()函数用于创建一个子进程,该子进程几乎复制了父进程的全部内容。我们能否让子进程执行一
个新的程序呢?exec 函数族就提供了一个在进程中执行另一个程序的方法。它可以根据指定的文件名或目
录名找到可执行文件,并用它来取代当前进程的数据段、代码段和堆栈段。在执行完之后,当前进程除了
进程号外,其他内容都被替换了。这里的可执行文件既可以是二进制文件,也可以是 Linux 下任何可执行
的脚本文件。

在 Linux 中使用 exec 函数族主要有两种情况。
① 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用 exec 函数族中的任意一个函数
让自己重生。
② 如果一个进程想执行另一个程序,那么它就可以调用 fork()函数新建一个进程,然后调用 exec 函
数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。

进程 – execl / execlp

  #include  <unistd.h>
  int execl(const char *path, const char *arg, …);
  int execlp(const char *file, const char *arg, …);

在这里插入图片描述

执行ls命令,显示/etc目录下所有文件的详细信息

  if  (execl(“/bin/ls”, “ls”, “-a”, “-l”, “/etc”, NULL) < 0) {
     perror(“execl”);
  }  
  
  if  (execlp(“ls”, “ls”, “-a”, “-l”, “/etc”, NULL) < 0) {
     perror(“execlp”);
  }  

进程 – execv / execvp

#include  <unistd.h>
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

在这里插入图片描述

执行ls命令,显示/etc目录下所有文件的详细信息

  char  *arg[] = {“ls”, “-a”, “-l”, “/etc”, NULL};
  
  if  (execv(“/bin/ls”, arg) < 0) {
     perror(“execv”);
  }  
  
  if  (execvp(“ls”, arg) < 0) {
     perror(“execvp”);
  }  

进程 – system

#include  <stdlib.h>
int system(const char *command);

在这里插入图片描述
定义:
int system(const char * string);
表头文件:
#include<stdlib.h>
说明:
system()会调用fork()产生子进程, 由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令, 此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD信号会被暂时搁置, SIGINT和SIGQUIT 信号则会被忽略。
返回值:
如果system()在调用/bin/sh时失败则返回127, 其他失败原因返回-1。若参数string为空指针(NULL), 则返回非零值。如果system()调用成功则最后会返回执行shell命令后的返回值, 但是此返回值也有可能为system()调用/bin/sh失败所返回的127, 因此最好能再检查errno来确认执行成功。

#include<stdlib.h>

int main(int argc, const char *argv[])
{
	system("ls -al /etc/passwd /etc/shadow");
	return 0;
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Set_Mode/article/details/89882489