今天主要学习了多任务编程中进程,线程,程序
一、程序、进程、线程的概念
1.程序
程序指的是一些保存在磁盘上的指令的有序集合,通常用某种程序设计语言编写,运行于某种目标计算机体系结构上。程序是静态的,就好比一个电脑上的普通文件一般,没有任何执行的概念。
2.进程
进程是计算机中的软件程序关于某数据集合上的一次运行活动,用通俗的话来讲,进程就是一个程序的一次执行过程。进程是一个独立的可调度的任务,是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡;当系统在执行某个程序时,系统进行资源分配和调度的基本单位就是我们所说的进程,因此可以理解进程是操作系统结构的基础,是程序执行和资源管理的最小单位。
3.线程
线程就是指的是进程中的实际运行单位,它是操作系统中进行运算调度的最小单位。换句话说,线程是进程中的一个最小运行单位。每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看做程序执行的一条条线索,被称为线程。一个进程中至少有一个线程,不然没有存在的意义。线程就是CPU调度和执行的单位。例如:你在看视频(进程)的同时可以听到声音(线程),看到图像(线程)和字幕(线程)等。线程自己不拥有操作系统资源,但是该线程可与同属进程的其他线程共享该进程所拥有的全部资源。
进程是跑起来的程序
一个程序可以对应多个进程
程序 ———— 静态 ———— 硬盘
进程 ———— 动态 ———— 内存
4.为什么需要进程
为了描述和管理程序运行 动态过程
xxx .C :程序的源代码 程序 = 代码 + 数据 a.out : 可执行程序
进程 = pcb + 进程实体 PCB(Porcess Control Block) : 进程信息表(户口本)
PID (Process ID) USER S(Status状态)
二,进程的生命周期
UNIX
Linux
1.ps -elf | grep a.out 查看父子进程 包含pid ppid
2.ps -aux | grep a.out
ps -elf 和 ps -aux 是两种常见的使用方式,它们之间主要的区别在于它们展示的信息字段和格式。
ps -elf
-e 选项告诉 ps 显示所有进程。
-l 选项产生长格式的输出,包括更多关于进程的详细信息。
-f 选项则是全格式(full-format)的缩写,它与 -l 选项类似,但会包含更多的列,比如 UID(用户ID)、PID(进程ID)、PPID(父进程ID)、C(CPU 使用率)、STIME(启动时间)、TTY(终端类型)、TIME(CPU 时间)、CMD(命令行/命令名)等。
ps -elf 的输出通常包括进程树信息,因为它显示了每个进程的父进程ID(PPID)。
ps -aux
-a 选项显示所有用户的进程,包括其他用户的进程。
-u 选项以用户为主的格式来显示进程信息,它会显示进程的拥有者。
-x 选项显示没有控制终端的进程。
ps -aux 的输出包含几列关键信息,比如 USER(用户名)、PID(进程ID)、%CPU(CPU 使用率)、%MEM(内存使用率)、VSZ(虚拟内存大小)、RSS(常驻集大小,即物理内存使用量)、TTY(终端类型)、STAT(进程状态)、START(启动时间)、TIME(CPU 时间)以及 COMMAND(命令行/命令名)。
主要区别
信息内容:ps -elf 侧重于显示进程的层级结构(通过 PPID),而 ps -aux 提供了关于每个进程更全面的资源使用情况(如 CPU 和内存使用率)。
输出格式:虽然两者都提供了详细的进程信息,但它们的列和顺序有所不同。ps -elf 倾向于展示更详细的层级和状态信息,而 ps -aux 则更侧重于资源监控。
使用场景:当你需要查看进程的层级关系或了解特定进程的基本信息时,ps -elf 可能更合适。而当你需要快速了解系统资源的使用情况,比如哪些进程占用了大量 CPU 或内存时,ps -aux 会更加有用。
注意,不同的 Unix/Linux 系统(如 Linux、BSD、Solaris 等)中的 ps 命令可能支持不同的选项和输出格式,因此最好参考你正在使用的系统的 ps 命令手册页(通过 man ps 命令访问)来获取最准确的信息。
pstree -sp pid 号 用于查看父子之间进程的关系(树形图,包含PID号)
3.kill //给进程发信号
简介 kill -l
在Linux系统中,kill命令用于向运行中的进程发送信号,默认发送的信号是终止信号,会请求进程退出。kill(杀)可能会引起误解,实际上发送的信号可能与杀死进程无关。
我们最常使用到的kill命令为:
kill PID
kill -9 PID
前者为请求目标进程退出,后者为强制杀死目标进程。
1. kill PID
kill命令默认发送的信号是SIGTERM。该信号会被目标进程捕获,在收到这个信号以后目标进程可以做一些有用的操作(如保存数据),然后退出。然而,许多进程并没有专门实现处理此信号的程序,此时会调用默认的信号处理函数。而在某些情况下,有特殊处理程序的进程也会出错,无法正确处理信号。总之,SIGTERM信号不能确保目标进程能够退出。
SIGTERM信号的编号通常为15,可通过以下四种方式发送SIGTERM信号:
kill PID
kill -s TERM PID
kill -TERM PID
kill -15 PID
2. kill -9 PID
此时发送的是SIGKILL信号。正如前文所述,SIGTERM信号不一定能够“杀死”目标进程,在这种情况下,我们就会释放大杀器,SKGKILL信号。SIGKILL信号不会被进程所“截获”,它只能由主机系统内核处理,由其负责提供可靠的控制进程执行的方法,SIGKILL会杀死进程。
SIGKILL信号的编号通常为9,可通过三种方式发送SIGKILL信号:
kill -s KILL PID
kill -KILL PID
kill -9 PID
killall a.out 杀死所有a.out的进程
3. 其他kill信号
kill命令可以发送的信号还有很多,其它有用的信号包括HUP、TRAP、INT、SEGV及ALRM等。
HUP发送SIGHUP信号,SIGHUP信号的意思为挂起。在规范上,进程应当在收到这个信号时重新加载配置,相当于重启。但在实际使用中,通常只有守护进程会按照规范实施,而普通进程则是执行退出。
INT发送SIGINT信号,意为中断,在终端中,只需按下CTRL+C便可以产生SIGINT信号。
在终端中,CTRL+Z通常映射至SIGTSTP,CTRL+\(反斜杠)映射至SIGQUIT,这可强制程序进行核心转储。
二. kill能确保杀死进程吗?
答案是否定的,某些情况下即使kill -9也无法杀死进程。
4. 僵尸进程
进程停止后,该进程就会从进程列表中移除。但是,有时候有些进程即使执行完了也依然留在进程列表中。这些完成了生命周期但却依然留在进程列表中的进程,我们称之为 “僵尸进程”。
a. 僵尸进程的产生
一个进程可能会产生很多子进程。这些子进程执行完毕后会发送一个Exit信号然后死掉。这个Exit信号需要被父进程所读取。父进程随后调用wait命令来读取子进程的退出状态,并将子进程从进程列表中移除。但若父进程未能读取到子进程的Exit信号,则这个子进程不会从进程列表中删掉。
b. 找出僵尸进程
ps aux | grep Z
c. kill僵尸进程
我们可以分别通过SIGTERM信号、SIGKILL信号、SIGHUP信号来尝试kill僵尸进程。
kill PID
kill -9 PID
kill -HUP PID
如果僵尸进程没能kill掉,则可查看僵尸进程的PPID,找到父进程,令其回收子进程;如果无效,则可直接kill掉僵尸进程的父进程,父进程死后,僵尸进程成为”孤儿进程”,孤儿进程会被系统收养进入后台,32位为(init)64位为(systemd)过继给1号进程init,由init负责清理僵尸进程。
方法一,传递信号给父进程,命令其回收子进程的资源
kill -CHLD PPID
方法二,直接kill父进程,将此进程变成孤儿进程,交给init进程管理,由init进程回收此进程的资源
kill -9 PPID
R ———— 运行或可运行 ————就绪态
S ———— 可中断的睡眠态
D ———— 不可中断的睡眠态
T ———— 暂停态
Z ———— 僵尸态(避免出现)
三,进程编程
1.fork函数 创建一个父子进程
fork函数是UNIX或类UNIX系统中的一种分叉函数,其作用是创建一个与当前进程几乎完全相同的新进程,这个新进程被称为子进程,而原进程则被称为父进程。以下是关于fork函数的详细解析:
一、函数原型
fork函数的原型定义在<unistd.h>头文件中,其基本形式如下:
#include <unistd.h>
pid_t fork(void);
其中,pid_t是一个数据类型,用于表示进程ID,其实质是int类型。
二、功能描述
当调用fork函数时,系统会为新的子进程分配资源,并复制父进程的大部分内容(包括代码、全局变量、堆、栈、文件描述符等)到新进程中。然而,需要注意的是,虽然子进程在开始时与父进程几乎完全相同,但它们之后是独立执行的,拥有各自的内存空间和资源管理。
三、返回值
fork函数的返回值非常特别,它只被调用一次,但会返回两次,分别在父进程和子进程中返回不同的值:
在父进程中,fork函数返回新创建的子进程的进程ID(PID)。
在子进程中,fork函数返回0。
如果创建子进程失败,fork函数返回-1,并设置全局变量errno以指示错误原因。
四、使用场景
fork函数广泛应用于需要创建并发执行的多个进程的场景中,如处理并行任务、任务分割、实现守护进程等特殊的进程模式,以及创建进程树以实现复杂的进程关系和层次结构。
五、注意事项
资源复制:虽然fork函数会复制父进程的大部分内容到子进程中,但并非所有内容都会被复制。例如,文件描述符在父子进程中是共享的,但各自对文件描述符的修改(如移动文件指针)是独立的。
并发执行:父进程和子进程在fork函数之后是并发执行的,它们之间的执行顺序依赖于系统的进程调度策略。
错误处理:在使用fork函数时,应注意处理可能出现的错误情况,如资源耗尽、系统限制等。
僵尸进程:如果子进程先于父进程结束,而父进程没有调用wait或waitpid函数来回收子进程的状态信息,那么子进程将变成一个僵尸进程。僵尸进程会占用系统资源,因此应避免其产生。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == -1)
{
// 创建子进程失败
perror("fork failed");
return 1;
} else if (pid == 0)
{
// 子进程代码
printf("This is the child process, PID: %d\n", getpid());
}
else
{
// 父进程代码
printf("This is the parent process, PID: %d, Child PID: %d\n", getpid(), pid);
}
return 0;
}
在这个示例中,程序首先调用fork函数创建一个子进程。根据fork函数的返回值,分别在父进程和子进程中打印不同的信息。
1.每个进程拥有独立的4G内存空间(虚拟的)。
2.每个进程运行在各的4G空间中自独立。
2.每个进程间数据互相独立,不受影响。
3.父子进程运行的顺序不确定,取决于系统先调度谁。