一、什么是进程
1、程序
程序 = 数据结构 + 算法
数据:用来表示人们思维对象的抽象概念的物理表现 称为数据
指令:对数据的处理的规则 叫做指令
计算:对于有限的数据集合 所执行 目的在于解决某一个问题的一组有限指令的集合 称为计算
程序就是数据和指令的集合,一个程序的执行过程就是一个计算
2、程序的执行方式
(1)顺序执行
一个程序执行完,才能执行下一个程序
例子:
一般来说 一个程序分为三个步骤:
从键盘上获取数据 scanf()
计算
回显 printf()
缺点:CPU的利用率低,效率低
(2)并发执行
多个程序同时执行,互不影响
为了提高CPU的利用率,增加吞吐量
为了能够让程序并发执行,引入了进程的概念
3、进程
进程: “进行中的程序”
是具有独立功能的程序关于多个数据集合上的一次执行活动
例子:
gcc test.c -o test //test可执行程序
./test
// ps -ef
4、进程 和 程序 的区别
1)程序是静态的概念 (指令的集合)
进程是动态的概念 (执行活动,动态产生,动态消亡)
2)进程是程序的一次执行活动
一个程序可以对应多个进程
3)进程是一个独立的活动单位
进程是竞争系统资源的基本单位
5、进程的状态
操作系统把一个程序的执行过程,分为3个不同的状态
就绪态 Ready 准备工作已经做好了,只要等待CPU来执行
运行态 Running CPU正在执行该进程的指令
阻塞态 Blocked 等待 进程正在等待其他的外部事件
“就绪队列”
所有处于就绪态的进程 都在一个队列上排队等待CPU的调度
“调度程序”
任务调度,负责确定下一个进入 运行态的进程
“调度策略”
分时系统:调度策略 以“时间片轮转”为主要的调度策略
让每一个进程执行一段时间
Windows、 Linux 、...
实时系统:调度策略 以“实时策略”为主要调度策略
直到这个进程执行完毕 或者 主动放弃CPU 或者 被其他更高优先级抢占
才会指向下一个进程
进程要做的第一件事件 就是申请一块内存区域 来存储 程序的数据
不同的数据 属性是不一样
“分区域”来存储程序的数据
二、Linux进程地址空间的布局
“分段”
Linux对进程的数据进行分段管理,不同属性的数据,存放在不同的内存段中
不同的内存段 其属性和管理也就不一样了
1)代码段
.text 主要存到代码(用户代码),包括main主函数在内的所有用户自定义的函数
只读并且共享,这段内存 在程序的运行期间 不会被释放
.init 主要是存放系统给每一个用户自动添加的“初始化代码”
(例如:命令行参数、环境变量、... )
2)数据段
.data 主要存放程序 已经初始化好了的全局变量 和 已经初始好了的 static变量 (静态变量)
可读可写, 这段内存 在程序的运行期间 不会被释放
.bss 主要存放 未初始化的全局变量 和 未初始化的static变量 (静态变量)
可读可写, 这段内存 在程序的运行期间 会被释放
.bss 在进程初始化的时候(可能)被初始化为0 (编译器的优化)
.rodata 只读数据段read only
主要存放程序中的只读数据(例如:字符串常量 )
只读,这段内存 在程序的运行期间 不会被释放
3)栈 stack
主要存放 局部变量 (非static修饰的)
可读可写,这段内存 会自动释放
(代码块执行完毕,代码中的所有的局部变量的空间都会自动释放)
随代码块的持续性
4)堆 heap
动态内存空间
主要是 malloc / realloc / calloc 动态分配的空间
可读可写, 这段内存 在程序的运行期间,一旦分配成功,就会一直存在
直到 手动释放free() 或者 进程结束
练习:
int a; // .bss
int b = 9; // .data
int main()
{
char *s = "1234567890"; // s --> 栈 "1234567890" --> .rodata
char *s2 = malloc( 10 ); // s2 -->栈 s2指向的空间 --> 堆
int c = 4; // c --> 栈
static double d = 3.14; // d --> .data
char buf[] = "abcdef"; // 栈
static char f; // f --> .bss
}
三、Linux进程相关的接口函数
1)创建一个新进程 fork()
fork() 用来创建一个进程
pid_t 用这个类型来描述进程id
一个进程包含什么东西?
数据:用户、系统
指令
当用fork()去创建一个新进程的时候,这个新进程的数据和指令来源于哪里?
来源于 父进程
☆☆☆
fork()这个函数在创建子进程的时候
拷贝父进程的 数据 和 指令 :
父进程的变量、数据对象
标准IO的缓冲区
文件描述符
...
拷贝完毕,父子进程就独立了
fork()成功之后,就会有两个进程执行当前的代码
所以为了区分父子进程,fork()调用一次,会有两次返回
一个父进程返回
一个子进程返回
NAME
fork - create a child process
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:创建一个子进程
参数:
无
返回值:
成功,返回
父进程 返回子进程的pid pid > 0
子进程 返回0
失败,返回 -1,同时errno被设置
fork()函数内部实现的伪代码
pid_t fork(void)
{
//拷贝,一旦拷贝成功,就会有两个进程往下执行
clone();
if( 父进程 )
{
return 子进程pid ;
}
else if( 子进程 )
{
return 0;
}
else
{
return -1;
}
}
例子:
int main()
{
//创建一个子进程
pid_t pid = fork();
if( pid > 0 ) //父进程
{
printf("I am father !\n");
}
else if( pid == 0 ) //子进程
{
printf("I am child !\n");
}
else
{
perror("fork error ");
return -1;
}
printf("We are family !\n");
}
注意:
1)命令行默认在父进程结束之后立即显示
2)父进程和子进程的执行的先后顺序是随机的
1.1、额外获取pid函数
Linux会为每一个进程 分配一个唯一的进程id (>0) 整数
用 pid_t 类型 来描述
还提供了两个接口函数 用来获取 进程ID 和 父进程ID
getpid()
getppid()
NAME
getpid, getppid - get process identification
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:返回当前进程的id
pid_t getppid(void);
功能:返回父进程的id
2)进程退出
进程退出有两种情况
(2.1)自己退出
(a) main()主函数返回,进程结束
(b)在进程的任意时刻,调用进程的退出函数
exit() / _exit()
NAME
exit - cause normal process termination
SYNOPSIS
#include <stdlib.h>
void exit(int status);
功能:终止进程
参数:
status:表示退出码,退出状态
退出码的具体的含义,由程序员自己来解释 (类似于函数的返回值)
返回值:
无
注意:
exit正常退出,会做一下清理工作
(例如:把缓冲区的数据 同步到文件中 )
NAME
_exit - terminate the calling process
SYNOPSIS
#include <unistd.h>
void _exit(int status);
功能:终止进程
参数:
status:表示退出码,退出状态
退出码的具体的含义,由程序员自己来解释 (类似于函数的返回值)
返回值:
无
注意:
_exit直接终止,不会做清理工作
(2.2)他杀
3)等待子进程的退出
wait()
waitpid()
都是等待子进程退出,顺便帮子进程回收资源
NAME
wait, waitpid - wait for process to change state
SYNOPSIS
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程退出
参数:
wstatus:指针,指向的空间用来保存子进程的退出信息(如何死亡、退出码等)
返回值:
成功,返回退出的那个子进程的pid
失败,返回 -1,errno被设置
例子:
int status;
wait( &status );
解析退出信息:
WIFEXITED( status )
如果表达式为真,则表示该子进程正常退出
WEXITSTATUS( status )
返回子进程的退出码,只有在子进程是正常退出的情况下才有效
WIFSIGNALED( status )
如果表达式为真,则表示该子进程是被信号杀死的
WTERMSIG( status )
返回 导致该子进程死亡的信号值
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:等待指定的子进程退出
参数:
pid:指定要等待退出的那个进程的pid 或者 进程组
pid > 0 :等待指定的pid进程
pid == 0 表示等待 与调用进程同组的任意子进程
“进程组”
就是每一个进程 都必须属于某一个进程组
创建这个进程组的进程为组长,进程组ID就是组ID
pid == -1 表示等待 任意子进程
pid < -1 表示等待 组ID 等于 pid的绝对值 的那个组里面的任意一个子进程
例子:
pid == -123
等待组ID为123的进程组里面的任意一个子进程
wstatus:指针,指向的空间用来保存子进程的退出信息(如何死亡、退出码等)
options:等待选项
0 :阻塞等待
WNOHANG 非阻塞 如果没有可用的子进程,立即返回
返回值:
成功,返回退出的那个子进程的pid
失败,返回 -1,errno被设置
例子:
int main()
{
//创建一个子进程
pid_t pid = fork();
if( pid > 0 ) //父进程
{
printf("I am father !\n");
int wpid;
int status;
//wpid = wait( &status ); //等待子进程退出
//wpid = waitpid( pid, &status, 0 ); //等待子进程退出
wpid = waitpid( -1, &status, 0 );
printf("wait pid : %d\n", wpid );
if( WIFEXITED( status ) )
{
printf("exit code : %d\n", WEXITSTATUS( status ) );
}
else if( WIFSIGNALED( status ) )
{
printf("kill signal : %d\n", WTERMSIG( status ) );
}
}
else if( pid == 0 ) //子进程
{
printf("I am child !\n");
sleep(5);
//return 2;
exit(5);
}
else
{
perror("fork error ");
return -1;
}
printf("We are family !\n");
}
这两个函数都是 用来等待某个(某些)子进程的状态发生改变:
等待状态发生改变 有三种情况:
(1)子进程退出(正常退出): main主函数返回, exit(), _exit()
(2)子进程被信号终止
(3)子进程被信号唤醒
注意:
假如一个子进程的状态的已经发生改变了,那么调用 wait()/waitpid() 立即返回,不会阻塞等待
否则 阻塞等待某个子进程的状态发生改变 或者 被信号中断
在子进程正常退出的情况下,调用 wait()/waitpid() 可以释放子进程的资源
如果没有调用 wait()/waitpid() ,子进程的资源就不会被释放,就会变为 “僵尸进程” zombie process
“僵尸进程”
一个已经终止运行 但是 仍然占有系统资源的进程 称为僵尸进程
“孤儿进程”
是指 其父进程执行完毕或者被终止后,仍继续运行的子进程 称为孤儿进程
孤儿进程最终会被init进程收养,init进程会回收它们的资源
四、exec函数族
fork创建一个子进程,是让子进程去做其他任务
exec函数族
exec函数主要的作用 让一个进程去执行另一个指定的程序文件
也就是说,让指定的程序文件的数据和指令 替换 调用进程的数据和指令
exec让进程去执行另外一个程序,需要告诉这个进程什么东西?
指定要执行的这个程序的路径名
可能需要指定 程序运行的参数 (linux程序的命令行参数都是字符串)
l list
把程序运行的参数 一个一个列举出来
程序运行的参数 第一个是程序的文件名 (不带路径的)
最后一个参数 必须是 NULL
"sum", "123", "456", NULL
v vector数组
把程序运行的参数 都存放在一个char *数组中
char * buf[] = { "sum", "123", "456", NULL };
p path路径
是否指定可执行文件的路径
加p:不需要指定路径
e environment环境变量
传该程序的环境变量
execl / execlp / execle / execv / execvp / execvpe
NAME
execl, execlp, execle, execv, execvp, execvpe - execute a file
SYNOPSIS
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
功能:去执行一个指定的程序文件
参数:
path:指定程序文件的路径名
arg: 程序运行的参数
返回值:
成功,不返回
失败,返回-1,同时errno被设置
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
例子:
sum.c
int main( int argc, char *argv[] )
{
int s = atoi( argv[1] ) + atoi( argv[2] );
printf( "sum = %d\n", s );
return s;
}
execl( "./sum", "sum", "123", "456", NULL );
=====================
atoi() //字符串转整数
NAME
atoi - convert a string to an integer
SYNOPSIS
#include <stdlib.h>
int atoi(const char *nptr);
例子:
int x = atoi("123");
printf("%d\n", x); // 123
==============================================
system() 用来执行指定的命令
NAME
system - execute a shell command
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
功能:执行command指定的命令