引出
我们学习了进程,是为了去用多进程,那么,为什么需要用到多进程呢?
1:为了提高效率,支持大用户量的并发。
2:一些大型的服务器程序,是常年补下电的,需要一直运行,而服务器系统崩溃是很严重的事情,那么,就需要有多进程,一个进程挂掉了不能影响另外一个进程的使用,这样,对用户来说,是体会不到的,不影响业务的进行。
举例:银行怎么提高效率?多开几个窗口,每个窗口都在做业务。
举例:之前在华为的网关产品项目中使用多进程的举例。
一:进程的创建
一个现有进程(父进程)可以通过调用fork()函数来创建一个跟现有进程一模一样的新进程(子进程)。
头文件: #include <unistd.h>
函数原型:pid_t fork(void);pid_t 实际就是int类型。
返回值: 如果创建子进程成功,则返回给父进程的是子进程的id,返回给子进程的是0。
如果失败,则返回给父进程的是-1,并置errno。
注意:1:子进程创建的过程:会复制父进程的所有资源,包括堆,栈,rodata段,data段,bss段, 缓冲区。但是系统相关信息,代码段共享。
2:fork之后父,子进程谁先执行是不确定,取决系统的调度算法。
3:fork之后,父子进程都是从fork下一条语句开始执行。
4:fork之后,父子进程拥有独立的4G虚拟地址空间。互相不影响。
5:fork之后,子进程会继承父进程的打开的文件描述符集合,共享文件状态标志位和文件的偏移量。
例如:打开一个文件,在父亲进程中用文件描述符偏移到文件的中间。如果此时创建子进程,则子进程也是也是从文件的中间开始的。
步骤:1:int main(){fork();printf("hello\n");} hello打印了两遍,说明进程已经创建。
2:查看返回值,并且在父子进程中个分别打印自己的id。
3:解释下边代码运行结果打印两边hello的原因。(原因就是fork出来的子进程,会完全的复制父进程的所有资源,包括缓冲区)
printf("hello");
pid = fork();
===================================================================
僵尸子进程:子进程结束的时候,父进程没有进行收尸操作(父进程还存在),此时占用资源。
孤儿进程:父进程结束了,子进程会变成孤儿进程,会自动被init进程所收养。
===================================================================
我们看到man帮助中,有个copy_on_write这样的字眼,这个是什么东西?我们来拓展介绍一下。
《写时拷贝技术》
<2>vfork
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:创建子进程
参数:无
返回值:成功,对父进程而言。返回子进程的PID好。
对子进程而言。返回0.
错误,返回-1。
fork与vfork的区别:
<1>fork函数父子进程谁先运行不确定,由系统调度决定。
vfork函数子进程先运行,此时父进程会阻塞,子进程会一直运行在父进程的地址空间,直到子进程调用exit结束后才会运行,如果这时子进程修改了某个变量,这将影响到父进程的变量。
<2>fork 函数的正文段共享,其他段被子进程复制。
vfork函数的子进程直接共享父进程的虚拟地址空间。
<3> 来看一下父子进程对同一文件的操作
先open一个文件,再fork,然后分别再父子进程中写入内容,结果是追加写
先fork,然后分在父子进程中打开同一文件,然后分别写入内容,结果分别写
为什么?下边第一个图是两个独立的进程打开同一个文件,第二个图是先open,再fork之后父子进程对文件共享的关系图。文件表项是在内核空间,进程间共享的。
二:进程的退出
1:相关退出函数
<1>return 结束一个函数的执行。(当前程序不一定结束。)
<2>void exit(int status)[库函数]
功能:结束一个进程。结束之前会刷新缓冲区。
参数:@status 进程状态的标志。0表示正常结束,其他表示异常结束。
<3>void _exit(int status) [系统调用]
功能:结束一个进程。结束之前不会刷新缓冲区。
参数:@status
2:return和exit的区别
2.1. return返回函数值,是关键字;exit是一个函数。
2.2. return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
2.3. return是函数的退出(返回);exit是进程的退出。
2.4. return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。
2.5. return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。
2.6. 非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。
3:exit和_exit的区别
3.1:exit是库函数,_exit是系统调用,exit是基于_exit的实现。
3.2:exit退出进程会清理IO缓冲区,_eixt不会。
三:进程的替换(exec函数族)
1.环境变量
或者称为全局变量,存在与所有的shell 中,在你登陆系统的时候就已经有了相应的系统定义的环境变量了。Linux 的环境变量具有继承性,即子shell 会继承父shell 的环境变量。
查看当前系统的全部环境变量信息:env命令
修改当前系统环境变量信息:直接修改对应的环境变量的值(临时的,只再当前shell生效)
永久修改:修改配置文件(按照层级)
/etc/profile.d-> /etc/bashrc ->~/.bash_profile -> ~/.bashrc
~/.bash_profile 用户登录时被读取,其中包含的命令被执行。
~/.bashrc 启动新的shell时被读取,并执行。
需要重点关注的:PATH:决定了shell将到哪些目录中寻找命令或程序
举例:a.out的执行,不带./看是否可以?
修改PATH环境变量,把a.out的文件所在路径加进去,然后再次执行。
2. exec函数族
<1>头文件
#include <unistd.h>
<2>函数原型
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
返回值:成功返回0,失败返回-1。
参数:@param path: 可执行文件的路径名
@param file: 可执行文件名,只能搜索环境变量 PATH 指定的路径
@parma arg: 可执行文件名以及参数,参数列表需要以 NULL 结尾
@param argv[]: 参数数组,可以取代参数列表
@param evnp[]: 环境变量数组
应用举例:
execl("/bin/ls","ls","-l",NULL); ./a.out argv[0]:./a.out 1 + 2 argv[1]
execlp("ls","ls","-l",NULL);
char * const envp[] = {"USER=root","PATH=/bin",NULL};
execle("./app","./app","1",NULL,envp) ;
char * const argv[] = {"ls","-l",NULL};
execv("/bin/ls",argv);
char * const argv[] = {"ls","-l",NULL};
execvp("ls",argv) ;
char * const argv[] = {"ls","-l",NULL};
char * const envp[] = {"USER=root","PATH=/bin",NULL};
execvpe("ls",argv,envp);
.* 练习
1.自己写一个calc.c,要求实现加减乘除功能。gcc calc.c -o calc
./calc 12 + 20
2.自己写一个execl_home.c,要求使用execl调用calc打印相应的内容
参考代码:calc.c,main.c
calc.c:./calc 10 - 20 10
execl_home.c:./execl 10 + 20
.*练习
实现一个简单的shell。
问题描述参考《myshell的实现》
strtok函数的使用。
四:进程的等待
僵尸进程:子进程结束的时候,父进程没有进行收尸操作(父进程还存在),此时子进程还占用资源。这时候的子进程就是僵尸进程。
父进程回收子进程资源的时机:(1)父进程结束 (2)处理子进程结束时候发送的SIGCHILD信号来回收资源。
僵尸进程的危害:僵尸态子进程已经结束,它占用大部分资源已经释放,但是仍然保留PID资源。 如果父进程一直不退出,就一直不会回收子进程的僵尸资源,这样产生僵尸态子进程过多,会导致PID资源耗尽,创建子进程失败。
那为什么还要设计僵尸进程呢?给进程设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时间获取。这些信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间,内存使用量等等)。
解决办法:为了防止产生僵尸进程,在fork子进程之后我们都要wait它们;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。
<1>wait的用法
函数原型:pid_t wait(int *status)
函数功能:回收僵尸态子进程,如果没有僵尸态的子进程则阻塞,如果没有子进程会立即返回。
函数参数:@status 是一个整型指针,指向的对象用来保存子进程退出时的状态
a. status若是为NULL ,表示忽略子进程退出时的状态。
b. status若是不为NULL ,表示保存子进程退出时的状态。
返回值:成功返回僵尸态子进程的PID,失败返回-1(没有子进程)。
大部分的时候,我们不需要关注子进程退出时候的状态,只是想把这个僵尸子进程干掉,这个时候,我们就不需要传递实际的status获取状态,直接传NULL进去就可以了,但是,也有的时候,我们是需要关注这个状态的,比如我确实需要知道子进程是不是正常退出的,还是异常退出的。那我们呢就需要知道传出来的status不同的值代表什么意思。
WIFEXITED(status) 宏返回真表示进程正常退出
WEXITSTATUS(status) 取得子进程exit()返回的结束代码,一般会先用WIFEXITED来判断是否正常结束,然后才使用此宏。
===============================================
WIFSIGNALED(status) 如果子进程是因为信号而结束则,返回值为非0 。
否则,返回值为0。
WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED来判断后,然后才使用此宏。
注意:wait函数是以阻塞(暂停)方式等待子进程结束,等待当前父进程的任一子进程的退出!
如果是多个子进程,要实现对所有子进程的收尸操作,需要循环调用wait来实现!
思考:什么是阻塞,什么是非阻塞呢?
阻塞: 得到调用的结果之前。一直等待。直到获得了结果再去做其他的事情。
非阻塞:得到调用的结果之前。你可以做其它的事情。当获得了结果告诉我一声就可以了。
例如:exit(5) 结束子进程。则我们调用WEXITSTATUS(status)就返回5.
练习:
fork一个子进程,子进程打印自己的pid,然后死循环,(用信号终止子进程)
父进程wait子进程结束,要获得子进程终止的信号编号。
<2>waitpid的用法
waitpid 函数常见用法如下:
1. 使用非阻塞的方式等待特定子进程退出
while(waitpid(pid,&status,WNOHANG) == 0)
usleep(50000);
2. 阻塞等待任意子进程退出
waitpid(-1,&status,0);====wait(&status)
waitpid(-1,NULL,0);=====wait(NULL);
3. 非阻塞等待任意子进程退出
waitpid(-1,&status,WNOHANG);
4.阻塞等待特定子进程的退出
waitpid(pid,&status, 0);
.*练习
父 子
使用waitpid(pid, NULL,0)指定这个子进程,阻塞式的等待它退出
waitpid(-1, &sta, WNOHANG)非阻塞等待任意子进程退出,获取它的退出状态,正常退出打印退出码,因为信号退出,打印对应的信号值
网友评论