文章目录
进程相关概念
程序和进程
程序:程序是静态的概念,gcc xxx.c -o pro,磁盘中生成pro文件叫做程序
进程:进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程
如何查看系统中有哪些进程
ps -aux | grep init 查看init相关进程
top
进程标识符:每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证
pid=0 交换进程(swapper) 为进程调度
pid=1 init进程 为系统初始化
并发
并发是多个任务交替执行的,多个任务之间可能还是串行的。所有的并发处理都有排队等候,唤醒和执行这三个步骤。所以并发是宏观的观念,在微观上他们都是序列被处理的,只不过资源不会在某一个上被阻塞(一般是通过时间片轮转),所以在宏观上多个几乎同时到达的请求同时在被处理。如果是同一时刻到达的请求也会根据优先级的不同,先后进入队列排队等候执行。并发针对的是多个请求,比如:一个CPU,一个web服务,同时涌入多个请求,CPU需要交替切换的执行多个请求,而不是一个请求执行完成之后再执行下一个请求。并发的实质是一个物理CPU(也可以是多个物理CPU)在若干个程序之间多路复用,并发性是对有限物理资源强制行使 多用户共享以提高效率。
虚拟内存和虚拟地址空间
虚拟内存: 虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。
虚拟地址空间: 在32位的i386 CPU的地址总线的是32位的,也就是说可以寻找到4G的地址空间。我们的程序被CPU执行,就是0x000000000xFFFFFFFF这一段地址中。高1G的空间为内核空间,由操作系统调用,低3G的空间为用户空间,由用户使用。
CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。因为只有程序的一部分加入到内存中,所以会出现所寻找的地址不在内存中的情况(CPU产生缺页异常),如果在内存不足的情况下,就会通过页面调度算法来将内存中的页面置换出来,然后将在外存中的页面加入到内存中,使程序继续正常运行。
为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都是一样的,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。
每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出)(这个就是虚拟内存),在需要的时候再装载回物理内存(换入)。
每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出)(这个就是虚拟内存),在需要的时候再装载回物理内存(换入)。
那既然有了虚拟地址空间,那必然要把虚拟地址「映射」到物理地址,这个事情通常由操作系统来维护。
那么对于虚拟地址与物理地址的映射关系,可以有分段和分页的方式,同时两者结合都是可以的。
内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致内存碎片和内存交换效率低的问题。
于是,就出现了内存分页,把虚拟空间和物理空间分成大小固定的页,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。
再来,为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。
Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。另外,Linxu 系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。
进程控制块PCB
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是 task_struct 结构体。
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h
文件中可以查看 struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
进程状态
进程基本的状态有 5 种。分别为新建态,就绪态,运行态,阻塞态与终止态。其中新建态为进程准备阶段,常与就绪态结合来看
- NULL→新建态:执行一个程序,创建一个子进程。
- 新建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
- 运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
- 运行态→就绪态:运行时间片到;出现有更高优先权进程。
- 运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
- 就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
- 等待态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
- 终止态→NULL:完成善后操作。
创建进程
基本API
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);失败返回-1;成功返回:父进程返回子进程的 ID(非负)如果是子进程返回 0
pid_t getpid(void);获取当前进程 ID
pid_t getppid(void);获取当前进程的父进程 ID
pid_t vfork(void);失败返回-1;成功返回:父进程返回子进程的 ID(非负)如果是子进程返回 0
补充: 初学者常常有个问题,就是子进程的执行范围:其实子进程只会继续执行fork函数之后的部分…
vfork和forl的区别
1)fork(): 父子进程的执行次序不确定。
vfork():保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。
2)fork(): 子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。
vfork():子进程共享父进程的地址空间(准确来说,在调用 exec(进程替换) 或 exit(退出进程) 之前与父进程数据是共享的)
案例程序
打印各自的pid号
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid>0) //如果大于0,大于0的数表示为创建子进程的pid号 这是父进程
{
printf("father:My father pid = %d\n",getppid()); //打印父进程pid
printf("father:My child pid = %d\n",pid); //打印子进程pid
printf("father:My pid = %d\n",getpid()); //打印自己的pid
}
else if(pid==0){ //这是子进程
printf("child:My father pid = %d\n",getppid()); //打印父进程pid
printf("child:My pid = %d\n",getpid()); //打印自己的pid
}
return 0;
}
模拟网络请求
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
char input[100];
int data;
while (1)
{
printf("请输入一个整数: "); //打印提示信息
if (fgets(input, sizeof(input), stdin) == NULL) { //获取输入数据
printf("读取输入错误.\n");
exit(EXIT_FAILURE); //这里表示程序执行失败,一般为非0整数
}
data = atoi(input); //将输入数据装换成整数
if (data == 0 && strcmp(input, "0\n") != 0) { //如果data为0并且input里面的数据大于"0\n"则判断转换失败
printf("输入非法: %s\n", input);
continue;
}
if(data==1){ //判断数字是否为1
pid = fork(); //创建进程
if(pid>0) //如果大于0,大于0的数表示为创建子进程的pid号 这是父进程
{
}
else if(pid==0){ //这是子进程
while(1){
printf("进行网络请求,进程号:%d\n", getpid()); //打印进程号
sleep(3);
}
}
}
else{
printf("wait,do nothing\n");
}
}
return 0;
}
vfork案例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork(); //创建进程
if(pid>0){ //如果大于0,大于0的数表示为创建子进程的pid号 这是父进程
while(1){
printf("father:cnt : %d pid : %d\n",cnt,getpid());
sleep(3);
}
}
else if(pid==0){ //这是子进程
while(1){
cnt++;
printf("child:cnt : %d pid : %d\n",cnt,getpid());
if(cnt>=3){ //运行三次结束自己的进程
exit(0);
}
sleep(3);
}
}
return 0;
}
应用场景
- 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的-----父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
- 一个进程要执行不同的程序。这对shell使常见的情况。在这种情况下,子进程从fork返回后立即调用exec
进程退出
相关概念
正常退出:
- Main函数调用return
- 进程调用exit(),标准C库
- 进程调用_exit()或者_Exit(),属于系统调用
异常退出:
- 调用abort
- 最后一个线程调用pthread_exit
- 最后一个线程从启动历程返回
- 接到一个信号并终止
- 最后一个线程对取消请求做出响应
exit()与_exit()的区别
return与exit的区别
主要来说exit是系统调用级别的,代表着一个进程的结束。它会删除进程的内存空间,同时把错误信息返回给父进程,通常情况,exit(0)表示正常退出,exit(1)和exit(-1)表示程序异常退出,exit(2)表示程序找不到指定的文件。在整个程序中,只要调用exit就结束。
return是语言级别的,代表着调用堆栈的返回。如果在main函数中代表着结束本进程,如果不是,则返回上一层调用。
通常情况,exit(0)表示正常退出,exit(1)和exit(-1)表示程序异常退出,exit(2)表示程序找不到指定的文件。在整个程序中,只要调用exit就结束。
程序API
#include <stdlib.h>
#include <unistd.h>
void exit(int status);
void _exit(int status);
void _Exit(int status);
案例程序
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork(); //创建进程
if(pid>0){ //如果大于0,大于0的数表示为创建子进程的pid号 这是父进程
while(1){
printf("father:cnt : %d pid : %d\n",cnt,getpid());
sleep(3);
}
}
else if(pid==0){ //这是子进程
while(1){
cnt++;
printf("child:cnt : %d pid : %d\n",cnt,getpid());
if(cnt>=3){ //运行三次结束自己的进程
exit(0);
}
sleep(3);
}
}
return 0;
}
父进程等待子进程退出
相关概念
明确创建子进程的目的:
子进程的退出状态:
(1)不被收集
僵尸进程:
一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。
(2)被收集
wait,waitpid,waitid 收集子进程退出状态的API
进程等待函数waipid
相关API
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);//*status 地址
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
1.非空:子进程退出的状态放在它所指向的地址中。
2.空:不关心退出状态。
3.返回值:如果成功,返回终止子进程的ID;错误,-1。
关于linux的进程状态参考
Linux进程状态解析 之 R、S、D、T、Z、X
参数:
- status:输出型参数,获取子进程的退出状态码
- pid:pid = -1时,表示等待任意一个子进程;pid > 0,等待指定子进程
- options:
0:阻塞等待,默认行为(也就是干等着子进程运行完,什么都不干)
**WNOHANG:**非阻塞等待,相当于隔一小段时间去检测子进程是否结束,如果结束了,
则回收子进程;如果没有结束,则继续忙自己的事
返回值:
回收子进程以后,返回收集到的子进程的pid
如果设置了WNOHANG,若检测到子进程还在运行,则返回0
若检测到子进程运行结束,返回子进程的pid
关于参数status
status不能简单的当作整型来看,要从二进制的角度来看,32位下,整型转化为二进制有32个bit位
但是我们仅关注低16位
(1) 正常退出时
进程正常退出时,子进程会返回退出码,即退出状态,8-15位记录着正常退出时的退出码,既然是正常退出就不会收到中止信号,所以0~7位都是 0
(2) 异常退出时
进程异常退出时,一般会收到一个中止进程的信号,而且不会执行到return 这句,所以自然就没有退出码,为了知道发生了何种异常,我们使用低 7 位,也就是 0~6 来记录 “中止信号”
如何取出退出码和中止信号
方式一:
中止信号是低7位,也就是0~6 的位置,我们可以让status按位与0x7F,那么0111 1111 中1对应的位置会被保留 —>status & 0x7F
退出码是高8位,我们先将这8位二进制退出码右移8位到 较低的8位,然后像上面那样按位与0xFF
那么1111 1111 中1对应的位置会被保留 ——> (status >> 8) & 0xFF
方式二:
使用系统提供的宏函数WIFEXITED(status)、 WEXITSTATUS(status)
WIFEXITED(status) :判断进程是否正常退出,如果正常退出,返回true
WEXITSTATUS(status): 返回子进程的退出码
WIFSIGNALED(status):判断子进程是否被信号结束
WTERMSIG(status):获取结束子进程的信号类型
案例程序
wait阻塞等待
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid>0){ //如果大于0,大于0的数表示为创建子进程的pid号 这是父进程
wait(&status); //死等子进程退出 这里如果为空不关心退出状态
printf("child quit,status = %d\n",WEXITSTATUS(status)); //取出状态码
while(1)
{
printf("cnt=%d\n",cnt);
printf("this is father print pid = %d\n",getpid());
sleep(1);
}
}
else if(pid==0){ //这是子进程
while(1)
{
printf("this is child print pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt>=3) //运行三次结束自己的进程
{
exit(0);
}
}
}
return 0;
}
非阻塞的方式查询
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork(); //创建进程
if(pid>0){
//wait(&status);
int ret = waitpid(pid,&status,WNOHANG); //非阻塞的方式程序进程是否结束
while(ret==0){ //不断轮询
printf("father: polling\n");
ret = waitpid(pid,&status,WNOHANG);
sleep(1);
}
//子进程退出
printf("child quit,status = %d\n",WEXITSTATUS(status));
while(1)
{
printf("cnt=%d\n",cnt);
printf("this is father print pid = %d\n",getpid());//获取进程pid号
sleep(1);
}
}
else if(pid==0){
while(1)
{
printf("this is child print pid = %d\n",getpid()); //获取进程pid号
sleep(1);
cnt++;
if(cnt==3) //运行三次结束自己的进程
{
exit(0);
}
}
}
return 0;
}
exec族函数
可参考以下文章
linux进程—exec族函数(execl, execlp, execle, execv, execvp, execvpe)
示例程序
execl
//文件execl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
int main(void)
{
printf("before execl\n");
if(execl("./echoarg","echoarg","abc",NULL) == -1)
{
printf("execl failed!\n");
}
printf("after execl\n");
return 0;
}
//文件echoarg.c
#include <stdio.h>
int main(int argc,char *argv[])
{
int i = 0;
for(i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
return 0;
}
实验说明:
我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径bin目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。
execlp可运行环境变量,当然也兼容上一个demo功能
//文件execlp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execlp(const char *file, const char *arg, ...);
int main(void)
{
printf("before execlp****\n");
if(execlp("ps","ps","-l",NULL) == -1)
{
printf("execlp failed!\n");
}
printf("after execlp*****\n");
return 0;
}
带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
//文件execvp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execvp(const char *file, char *const argv[]);
int main(void)
{
printf("before execlp****\n");
char *argv[] = {"ps","-l",NULL};
if(execvp("ps",argv) == -1)
{
printf("execvp failed!\n");
}
printf("after execlp*****\n");
return 0;
}
带e的一类exac函数,包括execle、execvpe,可以传递一个指向环境字符串指针数组的指针。 参数例如char *env_init[] = {“AA=aa”,”BB=bb”,NULL}; 带e表示该函数取envp[]数组,而不使用当前环境。
下面以execle函数为例:
//文件execle.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execle(const char *path, const char *arg,..., char * const envp[]);
char *env_init[] = {"AA=aa","BB=bb",NULL};
int main(void)
{
printf("before execle****\n");
if(execle("./echoenv","echoenv",NULL,env_init) == -1)
{
printf("execle failed!\n");
}
printf("after execle*****\n");
return 0;
}
//文件echoenv.c
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main(int argc , char *argv[])
{
int i;
char **ptr;
for(ptr = environ;*ptr != 0; ptr++)
printf("%s\n",*ptr);
return 0;
}
system函数
system(执行shell 命令)
相关函数
fork,execve,waitpid,popen
表头文件
#include<stdlib.h>
定义函数
int system(const char * string);
函数说明
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 来确认执行成功。
附加说明
在编写具有SUID/SGID权限的程序时请勿使用system(),system()会继承环境变量,通过环境变量可能会造成系统安全的问题。
注意,system() 函数的返回值是命令的退出状态码。如果命令执行成功,返回值为0,否则为非0值。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
char input[100];
int data;
while (1)
{
printf("请输入一个整数: "); //打印提示信息
if (fgets(input, sizeof(input), stdin) == NULL) { //获取输入数据
printf("读取输入错误.\n");
exit(EXIT_FAILURE); //这里表示程序执行失败,一般为非0整数
}
data = atoi(input); //将输入数据装换成整数
if (data == 0 && strcmp(input, "0\n") != 0) { //如果data为0并且input里面的数据大于"0\n"则判断转换失败
printf("输入非法: %s\n", input);
continue;
}
if(data==1){ //判断数字是否为1
pid = fork(); //创建进程
if(pid>0) //如果大于0,大于0的数表示为创建子进程的pid号 这是父进程
{
}
else if(pid==0){ //这是子进程
int ret = system("ls");
if(ret!=0){
printf("命令执行失败\n");
}
exit(1);
}
}
else{
printf("wait,do nothing\n");
}
}
return 0;
}
popen函数
提到system函数,就不得不提到popen函数,根据system函数的源代码:system函数的执行需要通过调用fork()函数创建一个子进程,子进程通过execl函数调用shell对传参的可执行文件进行实现。这也意味着system函数实现需要依赖execl函数实现自身功能。因此system函数的结果将直接显示在终端上,这样原本运行的结果就无法保存在文件中用于实现信息交互等功能。
NAME
popen, pclose - pipe stream to or from a process
SYNOPSIS
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
参数说明:
commmand:是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。
type:只能是读和写的一种,如果是 “r” 则文件指针连接到command的标准输出,则返回的文件指针是可读的;如果是 “w” 则文件指针连接到command的标准输入,则返回的文件指针是可写的。
stream:popen返回的文件指针。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
char ret[1024]={0};
FILE *fp;
fp = popen("ps","r");
int nread = fread(ret,1,1024,fp);
printf("read ret %d byte, ret = %s\n",nread,ret);
return 0;
}