Linux应用(3)——进程控制
一、简介
1.1 什么是进程
单进程:一个正在进行/运行的程序
多进程:多个正在进行/运行的程序
1.2 进程特点
能运行: 肯定存在main主函数
没结束: 程序运行没结束
内存空间:程序运行后,才变为进程,系统才会给进程分配一块运行空间
1.3 进程的用途
解决多功能的并发运行问题,当单核CPU需要并发执行多个功能时,采用裸机处理思想(标志位/中断)复杂度较高;为了解决该问题,引入了操作系统
M系列MCU中:引入小型的实时操作系统:ucos freertos liteos 系统中都是 多任务 概念
A系列MCU中:引入大型的分时操作系统:linux 鸿蒙 安卓 系统中都是多进程/多线程
多进程控制、通信 实际上就是应用算法: 主要用于系统的应用开发中
1.4 进程状态
简单认为进程状态: 就绪态、运行态、阻塞态、死亡态
R (running) :运行状态 表明进程要么是在运行中要么在运队列。
S (sleeping) :浅度睡眠状态
D (disk sleep) :不可中断睡眠状态 此状态只能自己醒过来,在这个状态的进程通常会等待IO的结束。
T (stopped) :停止状态
X (dead) :死亡状态
Z (zombie) :僵尸状态 此进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
1.5 进程与freertos任务对比
对比 | linux | freertos |
用途上 | 解决CPU多功能并发运行问题 | 解决CPU多功能并发运行问题 |
功能名称上 | 多进程、多线程 | 多任务 |
功能状态上 | 运行态、阻塞态、死亡态 | 运行态、阻塞态、死亡态 |
功能切换上 |
|
|
1.6 进程相关系统命令
命令1: ps -ef 用来查看进程的PID 和本身父进程的PPID
命令2: ps -aux 用来查看进程的CPU占用率、MEM占用率、 STAT状态
命令3: kill -信号值 进程PID 用来给指定进程发送信号
举例: kill -9 7689
1.7 进程相关名词
祖先进程:PID固定是1,是linux系统启动后运行的第一个进程
守候进程:孤儿进程就是守候进程的一种;脱离了终端的进程--后台进程
父子进程:是linux下进程的基本框架,任何一个进程都存在父进程(不是随便产生的)
孤儿进程:
父进程先于子进程结束,这时子进程就变成孤儿进程,这时子进程就会被祖先进程接收((也就类似于祖先进程是子进程的父进程了),
影响:孤儿进程并不会对系统造成危害,因为init进程会负责清理它们。实际上,这是Unix/Linux系统的一种机制,确保进程不会因为父进程的退出而失去管理。
僵尸进程:
子进程先于父进程结束,父进程没有回收子进程占用的内存空间,这时子进程就变成僵尸进程
影响:僵尸进程不占用系统资源(如内存),但是会占用进程表中的一个位置。如果大量僵尸进程积累,可能会导致系统无法创建新的进程(因为进程表已满)
总结:
孤儿进程:父进程先退出,子进程被init接管,不会造成危害。
僵尸进程:子进程退出,父进程未读取其退出状态,会占用进程表条目,可能造成资源浪费(进程表资源)。
所以从上可知:
1.孤儿进程、僵尸进程 都是针对于 子进程是孤儿还是僵尸
2.孤儿进程、僵尸进程都是不好的状态,都是代码的bug ,都需要优化代码
进程中代码优化(核心: 避免出现孤儿进程 或者僵尸进程)
---理想的结束状态:子进程先于父进程结束,父进程先回收子进程内存空间,然后在结束父进程
二、进程的控制方式
进程的控制方式存在两种:
方式1:在一个进程中运行另一个进程
方式2:在一个进程中创建另一个进程--父子进程
2.1 在一个进程中运行另一个进程
特点
- 另一个进程必须是存在且能用运行的
- 实现方式:采用 system 或 exec 函数
- 用途: 多功能菜单操作
函数
system 函数应用
头文件:#include <stdlib.h>
函数原型:int system(const char *command);
函数参数:const char *command : 要运行进程的可执行命令(系统命令、用户自定义命令)
函数返回值:-1: 执行失败
函数功能:执行一个可执行命令 (进程运行)
函数特性:当可执行命令中存在参数并参数通过外部得到时,可以借助于strcat或sprintf 完成完整地可执行命令,然后再作为实参传入举例:system("cd/tmp;mkdir test_dir;echo '创建成功'");
或system("cd/tmp && mkdir test_dir &&echo '创建成功'");
exec 函数簇
是由多个以exec作为函数名开端的同功能函数,是 Linux/Unix 系统编程中一组重要的函数,用于执行新的程序,替换当前进程的映像。这些函数在进程控制中起着核心作用。
头文件:#include <unistd.h>
函数原型:
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
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[]);
函数参数:
@param: const char *path : 包含可执行命令所在的绝对路径 举例 "/bin/ls"
@param: const char *file : 可执行命令名 举例 "ls"
@param: const char *arg, ... /* (char *) NULL */ : 包含可执行命令名的所有参数,必须以 NULL 结尾 举例 "ls","-l",NULL
@param: char *const argv[] : 把包含可执行命令名的所有参数封装到数组中,必须以NULL结尾,把数组作为实参 举例:char *const argv[] ={ "ls","-l",NULL};
@param: char * const envp[] : 设置的环境变量,必须以 NULL 结尾 "PATH=你所使用可执行命令所在的绝对路径", NULL 举例 "PATH=/bin", NULL
函数返回值: 调用是否成功
这些函数的区别在于:
1.参数传递方式:参数列表(l)还是数组(v)
2.是否使用环境变量PATH查找可执行文件(p)
如果 没有p : 可执行命令就需要绝对路径形式表示
如果 有 p : 可执行命令无需绝对路径
3.是否传递自定义环境变量(e)
如果 没有 e ; 不需要环境变量作为参数
如果 有 e : 需要环境变量作为参数
----从上述来看 使用比较简单的函数 execlp 或者 execvp
以 "touch 1.txt 2.txt" 举例
函数 execl应用: execl("/bin/touch", "touch","1.txt","2.txt",NULL);
函数 execlp 应用:execlp("touch","touch","1.txt","2.txt",NULL );
函数execle应用:
char * const envp[]={"PATH=/bin",NULL};
execle("/bin/touch", "touch","1.txt","2.txt",NULL, envp);
函数execv应用:
char *const argv[]={"touch","1.txt","2.txt",NULL};
execv("/bin/touch", argv);
函数execvp应用:
char *const argv[]={"touch","1.txt","2.txt",NULL};
execvp("touch", argv);
函数execvpe应用
char *const argv[]={"touch","1.txt","2.txt",NULL};// 参数数组
char * const envp[]={"PATH=/bin",NULL};//环境变量数组
execvpe("touch", argv, envp);
system与exec对比
system会阻塞当前进程直到命令执行完成,然后继续执行后续代码。
exec成功时不会返回,当前进程被新程序完全替换,执行完后原进程不再执行--通常会用在父子进程中,结束父子进程的运行
2.2 在一个进程中创建另一个进程
特点
- 另一个进程是不存在,所有要创建
- 新创建的进程---就是子进程;原来的进程--就是父进程
- 父子进程是多进程的一种特例
多进程: 多个独立的带有main主函数的程序
父子进程:还是只有一个main主函数,分成了父子进程 - 用途
父子进程: 主要用于处理2个关系比较密切的并发功能
多进程 : 主要用于处理多个关系不密切的并发功能
多线程 : 主要用于处理多个关系比较密切的并发功能
函数
fork vfork 进程创建
getpid getppid 得到自身进程PID ,和得到自身父进程的ppid
exit _exit return 进程的退出
wait waitpid 进程的等待(进程空间的回收)
fork vfork函数
头文件:#include <unistd.h>
函数原型:pid_t fork(void);
函数参数:无
函数返回值:pid_t
<0: 创建失败;
==0: 返回子进程操作
>0 : 返回父进程操作 (具体数值就是子进程的PID)
函数功能:
它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程函数执行顺序:
1.该函数调用一次,会依次产生两个返回值(==0 和 >0)
2.先返回哪个数值是不确定的,通常先返回 >0的
3.当返回>0数值时就表示进入父进程操作,在父进程中遇到阻塞类相关API函数,父进程就会进入阻塞态
4.父进程进入阻塞态,这时就再次返回 ==0数值,就表示进入子进程操作在子进程中遇到阻塞类相关API函数,子进程就会进入阻塞态
5.这时CPU就会在父子进程之间切换 (运行、就绪、阻塞、死亡)
6. fork前内容是父进程独有,只会执行一次;fork后内容父子进程共有,共有的内容会执行两次(父进程执行一次,子进程执行一次),所有为了操作的方便,fork后最好不要设计父子共有的内容,全部设计成父子独有内容(独有内容只会执行一次)函数特性:
1.该函数调用一次,会依次产生两个返回值(==0 和 >0)
2.写时复制:创建的子进程复制父进程的内存空间,父子进程占用不同的内存空间,父子进程内数据之间不会相互干扰
3.所以为了操作方便,在父子进程独有操作中单独定义数据,不要在fork前定义数据
4.资源继承: 子进程继承父进程的文件描述符也就是说:在fork前open的文件,是父子进程共有的
函数框架
函数框架
pid_t pid;//进程PID
pid =fork();// 进程创建
/* 在if之前添加的内容 就都是共有的*/
if(pid <0)// 基本不会执行到
{perror("fork error\n");return -1;
}
else if(pid==0)// 返回子进程操作
{/* 子进程独有*/
// 在这里定义子进程所需数据while(1){ }
}
else if(pid>0)// 或者 else 返回父进程操作
{/* 父进程独有*/// 在这里定义父进程所需数据while(1){ }
}
/* 在if形式后添加的内容 就都是共有的*/
getpid getppid 函数
头文件:#include<unistd.h>
函数原型:pid_t getpid(void);
函数返回值: 得到自身的PID
该函数在哪个进程中调用,就得到哪个进程自身的PID头文件:#include<unistd.h>
函数原型:pid_t getppid(void);
函数返回值: 得到自身父进程的PPID
该函数在哪个进程中调用,就得到哪个进程父进程的PPID
exit 、_exit、return 函数区别
进程的退出--结束
#include <stdlib.h>
void exit(int status);
函数参数: 会先刷新一下缓冲区,然后结束进程;结束进程时的返回状态 ,类似于 return 数值;
进程的退出--结束
#include <unistd.h>
void _exit(int status);
函数参数:不会刷新缓冲区,直接结束进程;
结束进程时的返回状态 ,类似于 return 数值;
举例1:
printf("hello\n");
exit(0);// 等价于 _exit(0);
举例2:
printf("hello");
exit(0);//运行结束:终端会输出 "hello"
举例3:
printf("hello");
_exit(0);//运行结束:终端不会输出 "hello"
1.return是C语言的关键字,exit、_exit是函数。
2.return是语言级别的,表示调用堆栈的返回。
3._exit是系统调用级别的函数,表示一个进程结束
4.exit函数是C函数库提供的用户级函数
return用于结束一个函数的执行,将函数的执行信息传出,给其他调用函数使用;
exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS(操作系统),这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。
非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的
wait,waitpid
头文件:#include<sys/types.h> ;#include<sys/wait.h>
函数原型:pid_t wait (int * status);
函数参数:int * status : 得到的子进程结束时的状态,也就是exit的实参,退出码必须在0~255之间 如果不想要子进程状态,那 NULL
函数返回值:执行成功则返回子进程PID,如果有错误发生则返回-1。
函数功能:阻塞式等待任意一个子进程结束,并回收子进程空间,避免留下僵尸进程
头文件:#include<sys/types.h>,#include<sys/wait.h>
函数原型:pid_t waitpid(pid_t pid,int * status,int options);
函数参数:
@param1 pid_t pid : 要等待的子进程PID
@param2 int * status : 得到的子进程结束时的状态,也就是exit的实参,
如果不想要子进程状态,那 NULL
@param3 int options: 等待状态
0 : 一直等待,直到等到指定子进程---常用
WNOHANG :不予以 等待
函数返回值:执行成功则返回子进程PID,如果有错误发生则返回-1。
函数功能:阻塞式等待指定子进程结束,并回收子进程空间
三、守候进程
守护进程(daemon)称为在后台运行且不受终端控制的进;
常用于提供系统服务或执行定期任务。
怎样让一个进程变为守候进程?
创建守护进程的步骤通常包括:
1.创建子进程,父进程退出(使子进程成为孤儿进程,并被init进程接管)
2.在子进程中创建新会话(setsid)
3.改变当前工作目录到根目录(避免占用可卸载的文件系统)
4.重设文件权限掩码(umask)
5.关闭不需要的文件描述符(特别是标准输入、输出、错误)
四、习题
1.孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程
2.僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程,但僵尸进程无法生成新的进程
僵尸进程必须使用waitpid/wait接口等待
父进程先于子进程退出,则子进程成为孤儿进程
孤儿进程运行在系统后台
孤儿进程会被以下init系统进程接管
exit函数退出一个进程时会刷新文件缓冲区
_exit函数退出一个进程时不会刷新文件缓冲区
编程题1:文件共享技术
若在fork()之前打开文件,则父子进程会共享这个文件的读写位置;若在fork()之后,由父子进程分别打开文件,则在父子进程中文件独立位置是彼此独立的。
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>// ./main 1.txt 2.txt
int main(int argc, char *argv[])
{off_t size;pid_t ret1;int fd1;fd1 = open(argv[1], O_RDWR|O_TRUNC);if(fd1 == -1){printf("file %s was not exist!\n", argv[1]);return -1;}ret1 = fork();//创建子进程if(ret1 == 0){//child only,运行子进程sleep(1); //阻塞1秒size = lseek(fd1, 0, SEEK_CUR);//由于fd1是在fork前执行的,父子共享,此时文件指针为5,当前位置偏移0后的相对于文件开始处绝对偏移量为5字节printf("size = %ld\n", size);//5exit(0);}else{//parent only,一般默认父进程先执行lseek(fd1, 0, SEEK_END);//从文件尾部移动0字节write(fd1,”hello”,5); //写入了5个字节,文件指针偏移5;wait(NULL); //等待子进程,代码运行到此处运行子进程exit(0);}
}
2.编程题
编写程序创建一个子进程,父进程打印“parent process”字样和自己的pid;子进程打印“child process”字样以及自己的pid和ppid,并通过exec更改代码段,执行cat命令,cat命令中的参数文件为自己的源程序文件。运行结果截图。注意:运行结果中子进程的ppid是否是父进程的pid?若不是,原因是什么,如何修改程序让两者相同?
不是,因为父进程已经结束,子进程已经变成孤儿进程,被祖先进程init(1)接收修改方式;在父进程中结束前 调用 wait 或 waitpid 函数
#include<sys/types.h>
#include<sys/wait.h>
#include <stdlib.h>int main()
{pid_t ret1;ret1=fork();if(ret1<0){printf("创建失败!\n");return -1;}else if(ret1==0)//进入子进程{pid_t childID,parentID;printf("child process\n");childID=getpid();parentID=getppid();printf("childID=%d,parentID=%d\n",childID,parentID);execlp("cat","cat","1.c",NULL);exit(0);}else//进入父进程{pid_t parentID;printf("parent process\n");parentID=getpid();printf("parentID=%d\n",parentID);wait(NULL);exit(0);}
}