本文目录
一、什么进程?
进程是一个计算机程序在执行中的实例。通俗地讲,进程就是一个正在运行的程序,它不仅包括程序的代码,还包括程序所使用的数据、程序的运行状态和操作系统为其分配的资源。
通俗来讲,想象你在电脑上打开了一个应用程序,比如一个文本编辑器。当你点击图标启动应用程序时,操作系统会创建一个进程来运行这个程序。这个进程包含了程序的所有代码和它需要的数据。你的电脑可以同时运行多个程序,比如你可以一边听音乐一边浏览网页。这是因为操作系统可以管理多个进程,并在它们之间切换,使它们看起来像是同时在运行。
进程有一个生命周期,从创建、执行到终止。操作系统负责创建进程,调度它们运行,并在它们完成工作后清理资源。
我们可以在linux系统上使用top
命令来查看系统的负载情况、各个进程的资源使用情况、内存使用情况、CPU 使用情况等。
按下数字1
可以查看多个cup详细使用情况。
二、进程地址空间划分
-
文本段(Text Segment):也称为代码段,用于存储程序的机器代码指令,通常是只读的。
-
数据段(Data Segment):包括初始化的全局变量和静态变量,以及显式初始化的局部静态变量。
-
BSS 段:包括未初始化的全局变量和静态变量,在程序启动时会被系统初始化为零值。
-
堆(Heap):用于动态分配内存,即运行时动态分配的内存空间。在堆上的内存需要由程序员显式申请和释放,通常使用 malloc()、calloc()、realloc() 等函数进行分配,使用 free() 函数进行释放。
-
栈(Stack):用于存储函数的局部变量、函数参数、函数调用的返回地址等信息。栈是一种先进后出(FILO)的数据结构,函数调用时会分配栈帧,函数返回时会释放栈帧。
三、进程的特点
- 独立性:每个进程都是独立的,它们运行在自己的内存空间中,不会直接干扰其他进程。这种独立性确保了一个进程的错误不会直接影响到其他进程。
- 资源拥有:进程拥有自己的资源,比如内存、CPU时间、文件句柄等。操作系统通过进程管理这些资源,并在进程间分配它们。
- 状态切换:操作系统可以在不同的进程之间切换,使用户感觉多个进程在同时运行。这种能力称为“多任务处理”。
四、进程创建
创建进程的方式有两种:fork
以及vfork
。
1. 相同点
创建进程时会返回两个返回值,这与平时的函数不同(平时的函数只有一个返回值)。返回值为0的为子进程。返回值大于0的为当前进程,也就是父进程。
无论是哪种创建方式,只要在fork
或vfork
创建进程后定义的变量或者其他内容,子进程和父进程都是一人一份,子进程只执行返回值等于0的程序,父进程执行返回值大于0的程序。两个进程互不干扰,各自执行各自的程序。
例如:我们使用fork创建进程后,定义了变量n=10,这里的变量n每个进程都有一份,互不影响,所以当父进程的n++时,并不会影响子进程中的n的内容,所以子进程中n还是为10。
#include <stdio.h>
#include <unistd.h>
int main(int avgc,char **argv)
{
pid_t pid;
pid=fork(); //创建进程
/*
从创建进程开始,下面的内容子进程和父进程每人一份。子进程只执行pid==0的程序,父进程执行pid>0的程序。两个进程互不干扰,各自执行各自的程序。
*/
int n=10;
if(pid==0)
{
while(1){
printf("我是子进程:%d\n",n);
sleep(1);
}
}
else if(pid>0)
{
while(1)
{
n++;
printf("我是父进程:%d\n",n);
sleep(1);
}
}
return 0;
2. 不同点
(1)执行顺序不同
fork: 创建的父子进程同时运行,执行顺序无先后之分,执行顺序不一定。
vfork:创建的父子进程,父进程必须要等子进程退出后才能执行。先执行子进程,后执行父进程。
举例如下:
●使用fork
#include <stdio.h>
#include <unistd.h>
int main(int avgc,char **argv)
{
pid_t pid;
pid=fork(); //方式一:fork创建进程
if(pid==0)
{
while(1){
printf("我是子进程\n");
sleep(1);
}
}
else if(pid>0)
{
while(1)
{
printf("我是父进程\n");
sleep(1);
}
}
return 0;
}
运行结果:
●使用vfork
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int avgc,char **argv)
{
pid_t pid;
pid=vfork(); //方式一:fork创建进程
int n=0;
if(pid==0)
{
while(1){
n++;
if(n>3) exit(0); //设置退出条件
printf("我是子进程\n");
sleep(1);
}
}
else if(pid>0)
{
while(1)
{
printf("我是父进程\n");
sleep(1);
}
}
return 0;
}
运行结果:
(2)复制与共享地址
fork:创建进程时,操作系统会复制一份父进程
的所有内容和整个地址空间,包括全局变量、代码段、数据段、堆和栈。子进程与父进程几乎完全相同,各自操作各自的变量和地址,互不影响。因为复制整个地址空间,fork 的性能开销相对较大,但现代操作系统通常通过“写时复制”技术来优化性能。
vfork :与 fork 不同,vfork 不会复制父进程的地址空间和内容。而是子进程与父进程共享在创建进程前
定义的所有变量以及地址空间,直到子进程调用 exec 或 _exit。即当父进程中的n++时,子进程中查看n时,也会进行改变。vfork 的设计目的是提高性能,特别是在立即调用 exec 执行新程序的情况下。
举例如下:
●使用fork
#include <stdio.h>
#include <unistd.h>
int main(int avgc,char **argv)
{
pid_t pid;
int n=0; //在创建进程前定义
pid=fork(); //创建进程
if(pid==0)
{
while(1){
n++;
printf("我是子进程,n=%d\n",n);
sleep(1);
}
}
else if(pid>0)
{
while(1)
{
n--;
printf("我是父进程,d=%d\n",n);
sleep(1);
}
}
return 0;
}
运行结果:
●使用vfork
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int avgc,char **argv)
{
pid_t pid;
int n=0;
pid=vfork(); //创建进程
if(pid==0)
{
while(1)
{
n++;
printf("我是子进程:%d\n",n);
sleep(1);
if(n == 5) exit(0); //循环5次,退出子进程
}
}
else if(pid>0)
{
while(1)
{
n++;
if(n==10) exit(0);
printf("我是父进程:%d\n",n);
sleep(1);
}
}
return 0;
}
运行结果:
五、进程退出
进程的退出除了使用Ctrl+c等强制退出手段外,还有两种自然退出方式:exit (0)
和_exit (0)
。
区别如下:
1.什么是缓存IO呢?
答:缓存IO是一种通过使用缓存技术来提高数据访问性能的输入输出操作方式。它主要涉及将频繁访问的数据存储在更快的存储介质(例如内存)中,以减少对较慢存储设备(例如硬盘、SSD)的访问次数和延迟。缓存I/O的主要目标是优化系统性能,提升数据读写效率。
举例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
while(1)
{
printf("hello");
sleep(1);
}
return 0;
}
我们运行该代码,发现并没有输出内容,这是为什么呢?
答:因为程序将” hello “存放到了IO缓冲区里,当IO缓冲区存满后或者遇到结束标志,如‘\n’。就会将缓冲区的内容输出到屏幕上。知道原理后,我们再写一个代码,清除的认识exit和_exit的区别。
2. exit()
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello");
exit(0);
}
我们使用exit结束进程时,发现屏幕上既然有内容输出。这是因为exit函数将缓存区的内容输出了出来。那么我们将exit函数换为_exit,会有什么效果呢?
3. _exit()
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello");
_exit(0);
}
我们运行程序,发现什么内容都没有输出。这是因为调用_exit()后,将缓冲区的内容全部丢弃导致的。这样我们就更清除的了解了这两个函数的区别。通常情况下我们都会使用exit()来结束进程。
六、进程状态
在操作系统中,进程的管理和状态转换是一个复杂且重要的机制。孤儿进程和僵尸进程是其中两个特定的状态,它们各自有不同的形成原因和处理方法。
1. 孤儿进程
定义:孤儿进程是指其父进程已经终止,但它自己仍在运行的进程。
在Unix和Linux系统中,孤儿进程会被操作系统自动重新父化给 init 进程(PID为1),即 init 进程成为这些孤儿进程的新父进程。init 进程负责监视这些孤儿进程并在它们终止时正确地回收它们的资源,避免资源泄露。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid=fork();
if(pid==0)
{
while(1){
printf("子进程\n");
sleep(1);
}
}
else if(pid>0)
{
printf("父进程\n");
exit(0); //父进程退出,子进程继续运行。
}
}
这时我们使用另一个终端使用ps -ef
查看进程。发现该进程的父进程已经变为1。我们可以使用 kill -9 7686
来结束该孤儿进程。
2. 僵尸进程
定义:子进程已经终止,但父进程尚未读取其退出状态,则子进程形成僵尸进程。
当一个子进程结束时,操作系统会发送一个信号(SIGCHLD)给父进程,通知它子进程已经结束。父进程需要调用wait()
或waitpid()
系统调用来读取子进程的退出状态。如果父进程没有执行这一操作,子进程的退出状态将一直保留在系统中,从而形成僵尸进程。
注意:一定要避免僵尸进程,因为僵尸进程会一直占用系统资源。而孤儿进程会有init进程来负责清理其资源。
僵尸进程如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid=fork();
if(pid==0)
{
printf("子进程\n");
exit(0);
}
else if(pid>0)
{
while(1){
sleep(1);
printf("父进程\n");
}
}
}
此时我们再打开一个终端,查看进程信息。发现下面标注的进程就为僵尸进程。
●使用等待函数,等待子进程退出,并清理其资源。
等待函数有:wait()
和waitpid()
。
wait 函数:会阻塞父进程,直到等待到任意一个子进程退出,并返回该子进程的 pid。
waitpid 函数:等待指定 pid 的子进程退出,并且它的行为不会被其他子进程所阻塞。如果为-1 表示等待所有子进程,同样返回该子进程的 pid。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options); //waitpid(pid, &n, 0)
//status 参数是传出参数,它会将子进程的退出码(进程的返回值)保存到 status 指向的变量里。如果不关心子进程的终止状态,可以传递NULL。
// int options :通常传0。
例程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid=fork();
if(pid==0)
{
printf("子进程\n");
exit(123); //设置退出号
}
else if(pid>0)
{
int n;
wait(&n); //等待子进程退出,
while(1){
sleep(1);
printf("父进程,%d号的子进程已退出\n", WEXITSTATUS(n));
}
}
}
这时我们查看后台进程,发现没有产生僵尸进程。
七、system函数
system 函数是一个标准的 C 库函数,用于在程序中执行 shell 命令。它提供了一种简单的方法来调用操作系统的命令解释器,并执行指定的命令字符串。运行代码后,会直接输出命令执行的内容。
#include "stdio.h"
#include "stdlib.h"
int main()
{
system("ls -al");
return 0;
}