目录
0. 进程概述
1. 创建进程
1.1 进程的创建:fork函数
1.2 进程的等待:wait()、waitpid()
1.3 特殊进程:僵尸进程、孤儿进程、守护进程
1.4 进程的终止:exit和_exit函数
1.5 进程退出清理:atexit函数
1.6 进程的创建 :vfork函数
2. 进程的替换:exec函数族
3. system函数:
总结:
0. 进程概述
进程的定义
程序和进程的区别:
- 程序:是静态的,存放在磁盘上的可执行文件。
- 进程:是动态的,是运行在内存中的程序的执行实例。
- 程序是一些指令的有序集合,而进程是程序执行的过程,进程是程序的一次执行过程。
- 进程的状态是变化的,其包括进程的创建、调度和消亡。
- 程序只要运行,此时就是进程,程序没运行一次,就会创建一个进程。
在Linux系统中,进程是管理事务的基本单元。
进程拥有自己独立的处理环境和系统资源(处理器、存储器、I/O设备、数据、程序)。
进程的状态及转换:
进程整个生命周期可以简单的划分为三种状态:
就绪态:
进程已经具备执行的一切条件,正在等待分配CPU的处理时间。
执行态:
该进程正在占用CPU运行。
等待态:
进程因不具备某些执行条件而暂时无法继续执行的状态。
进程的调度机制:
时间片轮转,上下文切换。
多进程不是说一个进程执行完再执行另一个进程,而是交替执行的,一个进程执行一段时间,然后下一个进程再执行一段时间,依次类推,所有进程执行完之后再回到第一个进程继续执行,依次类推。
进程三个状态的转换关系:
进程控制块:
进程控制块就是用于保存一个进程信息的结构体,又称之为PCB。
OS是根据PCB来对并发执行的进程进行控制和管理的。系统在创建一个进程的时候会开辟一段内存空间存放与此进程相关的PCB数据结构。
PCB是操作系统中最重要的记录型数据结构。PCB中记录了用于描述进程进展情况及控制进程运行所需的全部信息。
PCB是进程存在的唯一标志,在Linux中PCB存放在task_struct结构体中。
/usr/src/linux-heads....generic/include/linux/sched.h
PCB结构体中的部分数据:
调度数据:
进程的状态、标志、优先级、调度策略等。
时间数据:
创建该进程的时间、在用户态的运行时间、在内核态的运行时间等。
文件系统数据:
umask掩码、文件描述符表等。
内存数据、进程上下文、进程标识(进程号)
。。。
进程控制:
进程号:
每一个进程都是由一个进程号来标识,其类型为pid_t,进程号的范围:0-32767。
进程号总是唯一的,但是进程号可以重用。当一个进程终止后,其进程号就可以再次使用了。
在ubuntu中查看当前系统中所有的开启的进程:
ps ajx
PPID:当前进程的父进程的进程号。
PID:当前进程的进程号。
PGID:当前进程所在的组的进程组ID。
COMMAND:当前进程的名字
特殊的进程号:
- 在Linux系统中进程号由0开始。
- 进程号为0及1的进程由内核创建。
- 进程号为0的进程通常是调度进程,常被称为交换进程(swapper)。
- 进程号为1的进程通常是init进程,init进程是所有进程的祖先。
- 除调度进程外,在Linux下面所有的进程都由init进程直接或间接创建。
进程号(PID)
标识进程的一个非负整型数。
父进程号(PPID)
任何进程(除Init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
进程组号(PGID)
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。
Linux操作系统中提供了三个获得进程号的函数getpid()、getpgid()、getppid()。
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
功能:获取当前进程的进程号。
pid_t getppid(void);
功能:获取当前进程的父进程的进程号。
pid_t getpgid(pid_t pid);
功能:获取当前进程所在进程组的进程号。
代码示例:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgid = %d\n", getpgid(getpid()));
while (1);
return 0;
}
执行截图:
1. 创建进程
1.1 进程的创建:fork函数
fork函数:
#include<unistd.h>
pid_t fork(void);
功能:在已有的进程基础上创建一个子进程。
参数:
无
返回值:
成功:
>0:子进程的进程号,标识父进程的代码区。
==0:子进程的代码区。
失败:
-1:返回给父进程,子进程不会创建。
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。
地址空间:
包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的。
fork函数执行完毕后父子进程的空间示意图:
创建子进程
不区分父子进程(不推荐)
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
//通过fork函数创建一个子进程
//注意:只要执行了一次fork,就会在原有的进程基础上创建一个新的子进程
//而且如果fork之后不区分父子进程的代码区,则后面的所有代码都会执行
fork();
printf("hadj{\n");
while (1);
return 0;
}
执行截图:
区分父子进程(推荐)
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
//父子进程是来回交替执行的,谁先谁后不一定。
pid_t pid;
pid = fork();
if (pid < 0) //创建子进程失败
{
perror("fail to fork");
exit(1);
}
else if (pid > 0) //父进程代码区
{
while (1)
{
printf("in parent process\n");
sleep(1);
}
}
else //子进程代码区(pid == 0)
{
while (1)
{
printf("in son process\n");
sleep(1);
}
}
return 0;
}
执行截图:
父进程拥有独立的地址空间、
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int a = 555;
int main()
{
pid_t pid;
pid = fork();
static int b = 777;
int c = 888;
//子进程会复制父进程fork之前的所有内容
if (pid < 0)
{
perror("fail to fork");
exit(1);
}
else if (pid > 0)
{
printf("in partent process\n");
a++;
b++;
c++;
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
else
{
sleep(1);
printf("in son process\n");
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
while (1);
return 0;
}
执行截图:
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd;
if ((fd = open("file.txt", O_RDONLY)) == -1)
{
perror("fail to open");
exit(1);
}
//子进程会继承父进程的一些共有的区域,例如磁盘空间,内核空间。
//文件描述符的偏移量保存在内核空间中,所以父进程改变偏移量,子进程获取的偏移量是改变之后的。
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork");
exit(1);
}
else if (pid > 0)
{
printf("in parent process\n");
char buf[32] = "";
if (read(fd, buf, 30) == -1)
{
perror("fail to read");
exit(1);
}
printf("buf = [%s]\n", buf);
}
else
{
sleep(1);
printf("in son process\n");
char buf[32] = "";
if (read(fd, buf, 30) == -1)
{
perror("fail to read");
exit(1);
}
printf("buf = [%s]\n", buf);
}
while (1);
return 0;
}
执行截图:
子进程继承父进程的空间
进程的挂起:
进程在一定的时间内没有任何动作,称为进程的挂起。
#include<unistd.h>
unsigned int sleep(unsigned in sec);
功能:
进程挂起指定的秒数,知道指定的时间用完或受到信号才解除挂起。
返回值:
若进程挂起到sec指定的时间则返回0,中断则返回剩余秒数。
注意:
进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到就绪态。
代码示例:
#include<unistd.h>
#include<stdio.h>
int main()
{
while(1)
{
printf("hello world\n");
sleep(2);
}
return 0;
}
执行截图:
1.2 进程的等待:wait()、waitpid()
父子进程有时需要简单的进程间同步,如父进程等待子进程的结束。
Linux下提供了两个等待函数wait()、waitpid()。
wait函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被启用
若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
参数:
status:函数返回时,参数status中包含子进程退出时的状态信息。
子进程的退出信息在一个int中包含了多个字段,
用宏定义可以取出其中的每个字段。
子进程可以通过exit或_exit函数发送退出状态。
返回值:
成功:子进程的进程号。
失败:-1
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork");
exit(1);
}
if (pid > 0)
{
//不接收子进程的退出状态
wait(NULL);
//接收子进程退出状态,子进程中必须用exit或_exit函数退出进程时发送退出状态
int status = 0;
wait(&status);
if (WIFEXITED(status) != 0)
{
printf("son process return status:%d\n", WEXITSTATUS(status));
}
printf("in pratent proces\n");
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("in son process\n");
sleep(1);
}
exit(2);
}
return 0;
}
执行截图:
waitpid函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid:指定的进程或者进程组。
pid>0:等待进程ID等于PID的子进程
pid+0:等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会等待它。
pid=-1:等待任一子进程,此时waitpid和wait作用一样。
pid<-1:等待指定进程组中的任何子进程,这个子进程组的ID等于pid的绝对值
status:保存子进程退出时的状态信息。
options:选项
0:同wait,阻塞父进程,等待子进程退出。
WNOHANG:没有任何已经结束的子进程,则立即返回。
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(跟踪调试,很少使用)
返回值:
成功:返回状态改变了的子进程的进程号;如果设置了选项WNOHANG并且pid指定的进程存在,返回0。
失败:返回-1。当pid指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid 就会出错返回,这时 errno 被设置为 ECHILD。
wait(status) <==> waitpid(-1, status, 0)
代码示例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork");
exit(1);
}
else if (pid > 0)
{
waitpid(pid, NULL, 0);
printf("in father process\n");
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("in son process\n");
sleep(1);
}
}
return 0;
}
执行截图:
1.3 特殊进程:僵尸进程、孤儿进程、守护进程
- 僵尸进程(Zombie Process) 进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵尸进程。 子进程已运行结束,父进程未调用 wait 或 waitpid 函数回收子进程的资源是子进 程变为僵尸进程的原因。
- 孤儿进程(Orphan Process) 父进程运行结束,但子进程未运行结束的子进程。
- 守护进程(精灵进程)(Daemon process) 守护进程是个特殊的孤儿进程,这种进程脱离终端,在后台运行。
1.4 进程的终止:exit和_exit函数
exit函数:
#include<stdlib.h>
void exit(int value)
参数:
status:返回给父进程的参数(低 8 位有效)。
一般失败设置为非0,成功设置为0
_exit函数:
#include void _exit(int value)
参数:
status:返回给父进程的参数(低 8 位有效)。
一般失败设置为非0,成功设置为0
exit和_exit函数的区别:
- exit为库函数,而_exit为系统调用。
- exit会刷新缓冲区,但是_exit不会刷新缓冲区
- 一般使用exit
代码示例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void fun()
{
printf("nihao hangzhou");
//测试return是带换行\n
// printf("nihao hangzhou\n");
//主函数中退出进程,子函数中退出当前函数。
// return ;
//退出一个进程,刷新缓冲区
//exit(0);
//退出一个进程,不刷新缓冲区
_exit(0);
printf("welcoom to binjiang\n");
}
int main()
{
printf("hello world\n");
fun();
printf("hello kitty\n");
return 0;
}
执行截图:
return截图:
exit截图:
_exit截图:
1.5 进程退出清理:atexit函数
进程在退出前可以使用atexit函数注册退出函数
#include<stdlib.h>
int atexit(void (*function)(void));
功能:
注册进程正常结束前调用的函数,进程退出执行注册函数。
参数:
function:进程结束前,调用函数的入口地址。
一个进程中可以多次调用atexit函数注册清理函数,正常结束前调用函数的顺序和注册时的顺序相反。
代码示例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void clear_fun1(void)
{
printf("perform clear fun1 \n");
}
void clear_fun2(void)
{
printf("perform clear fun2 \n");
}
void clear_fun3(void)
{
printf("perform clear fun3 \n");
}
int main()
{
//atexit在进程结束时才会执行对应的回调函数
//注意调用顺序是反的
atexit(clear_fun1);
atexit(clear_fun2);
atexit(clear_fun3);
printf("process exit 3 sec later!!!\n");
sleep(3);
return 0;
}
执行截图:
1.6 进程的创建 :vfork函数
vfork函数
#include<sys/types.h>
#include<unistd.h>
pid_t vfork(void)
功能:
vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。
返回值:
创建子进程成功,则在子进程中返回0,父进程中返回子进程ID。
出错返回-1.
fork和vfork函数的区别:
vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。
vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。
相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间。
子进程在父进程之前运行
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
//使用vfork,子进程会先执行,直到子进程执行exit或exec后,父进程才会执行。
pid_t pid;
pid = vfork();
if (pid < 0)
{
perror("fail to vfork");
exit(1);
}
else if (pid > 0)
{
while (1)
{
printf("in father process\n");
sleep(1);
}
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("in son process\n");
sleep(1);
}
exit(0);
}
return 0;
}
执行截图:
子进程与父进程共享一个空间
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int a = 10;
int main()
{
//父子进程共享一个空间
pid_t pid;
int b = 9;
pid = vfork();
if (pid < 0)
{
perror("fail to vfork");
exit(1);
}
else if (pid > 0)
{
printf("in father process a = %d b = %d\n", a, b);
}
else
{
a++;
b++;
printf("in son process a = %d b = %d\n", a, b);
exit(0);
}
return 0;
}
执行截图:
2. 进程的替换:exec函数族
exec函数族,是由六个exec函数组成的
- exec函数族提供了六种在进程中启动另一个进程的方法
- exec函数族可以根据指定的文件名或目录名找到可执行文件
- 调用exec函数的进程并不创建新的进程,故调用exec前后,进程的进程号并不会改变,其执行的程序完全由新的程序替换,而新的程序则从其main函数开始执行。
exec函数族取代调用进程的数据段、代码段和堆栈段。
#include<unistd.h>
int execl(const char *path, const char *arg, .../*(char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execlv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, .../*, (char *) NULL*/, char *const envp[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
功能:
在一个进程里面执行另一个程序,主要用于执行命令。
参数:
path:命令或者程序的路径。
l:如果是带l的函数,对应的命令或者程序是通过每一个参数进行传递的,最后一个为NULL表示结束。
例如:"ls","-l",NULL
v:如果是带v的函数,对应的命令或者程序是通过一个指针数组来传递的,指针数组的最后一个元素为NULL标志结束。
char *str[] = {"ls", "-l", NULL};
p:如果是不带p的函数,第一个参数必须传当前命令或者程序的绝对路径,
如果是带p的函数,第一个参数既可以是绝对路径,也可以是相对路径。
返回值:
失败:-1
代码示例:
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork");
exit(1);
}
else if (pid > 0)
{
printf("in father process\n");
wait(NULL);
printf("the soon process has quied\n");
}
else
{
printf("in son process\n");
#if 0
if (execl("/bin/ls", "ls", "-l", NULL) == -1)
{
perror("fail to execl");
exit(1);
}
#endif
#if 0
if (execlp("ls", "ls", "-l", NULL) == -1)
{
perror("fail to execlp");
exit(1);
}
#endif
#if 0
char* str[] = { "ls", "-l",NULL };
if (execv("/bin/ls", str) == -1)
{
perror("fail to execv");
exit(1);
}
#endif
#if 0
if (execlp("./myshell.sh", "./myshell.sh", NULL) == -1)
{
perror("fail to execlp");
exit(1);
}
#endif
#if 1
if (execlp("./myshell.sh", "./myshell.sh", NULL) == -1)
{
perror("fail to execlp");
exit(1);
}
#endif
#if 0
if (execlp("./hello", "./hello", NULL) == -1)
{
perror("fail to execl");
exit(1);
}
#endif
#if 0
if (execl("./hello", "./hello", NULL) == -1)
{
perror("fail to execl");
exit(1);
}
#endif
printf("hello world\n");
}
return 0;
}
80, 1 Bot
一个进程调用exec后,除了进程ID,进程还保留了下列特征不变:
- 父进程号
- 进程组号
- 控制终端
- 根目录
- 当前工作目录
- 进程信号屏蔽集
- 未处理信号
- .....
3. system函数:
#include<stdlib.h>
int system(const char *command);
功能:
system会调用fork函数产生子进程,子进程调用exec启动/bin/sh -c string来执行参数string字符串所代表的命令,此命令执行完后返回调用进程。
参数:
要执行的命令的字符串。
返回值:
如果command为NULL,则system()函数返回非0,一般为1.
如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。
注意:
system调用成功后会返回执行shell命令后的返回值。其返回值可能为1、127也可能为-1,故最好应再检查errno来确认执行成功。
代码示例:
#include<stdio.h>
#include<stlib.h>
int main()
{
system("clear");
system("ls -l");
system("./hello");
system(. / myshell.sh");
return 0;
}
总结:
本章介绍了进程的概念、创建和使用。通过了解进程管理的核心原理,我们可以更好地利用和控制系统资源,实现并发操作,并构建高效的应用程序。深入了解进程的概念和技术,将使我们能够设计出更稳定、可扩展的系统架构,并解决多进程之间的通信和同步问题。
希望这篇博客能帮助您在应用程序开发过程中更好地理解和应用进程管理的重要性。谢谢阅读!