目录
一.进程创建
1.1fork函数
介绍:在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1.
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
[LF@ecs-100710 lesson4]$ ls
makefile test test.c
[LF@ecs-100710 lesson4]$ cat test.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("Before: id =%d\n", getpid());
pid_t id=fork();
printf("After:id =%d\n", getpid());
return 0;
}
[LF@ecs-100710 lesson4]$ ./test
Before: id =18398
After:id =18398
[LF@ecs-100710 lesson4]$ After:id =18399
这里看到了三行输出,一行before,两行after。进程9045先打印before消息,然后它又打印after。另一个after 消息有9046打印的。注意到进程9046没有打印before,为什么呢?如下图所示:
一般情况下,fork之后,父子进程共享所有代码,但子进程一般从fork之后开始执行(程序计数器会拷贝给子进程)
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork之后,那为什么要给子进程程返回0,给父进程返回子进程的id?
首先父子进程的 1 : n 的关系,所以在父子进程的立场中,父进程不需要标识,子进程需要标识。
其次子进程是要执行任务的,父进程需要区分子进程,所以给父进程返回子进程的pid,
因为父进程可以通过这个pid来区分是哪个子进程。给子进程返回0,
本质上是因为子进程不需要访问父进程pid,因为子进程也不需要知道父进程pid,
子进程不需要管理父进程,任务是给子进程的,它只需要知道自己调用成功了就可以。
1.2.写时拷贝
概念:linux当中父子进程代码和数据是共享的前提是建立在不发生修改的情况下,当任何一方尝试对其进行修改就会发生写时拷贝,子进程和父进程各自私有一份。
关于写实拷贝的几个常见问题:
如何理解父子进程代码和数据是共享的?
父子进程对应的页表指向的是同一块物理内存。当任何一方写入的时候,便会通过写时拷贝
的方式在物理内存中开辟空间,供那一方写入,但父子进程中虚拟地址并没有改变,不过它
们与物理内存映射的关系不一样了。
为何不在创建的时候就写时拷贝?
当父子进程都不对代码进行修改时,父子进程可以共享一块空间这样就减少了空间的浪费。
子进程不一定会使用父进程的所有数据,写入,本质是需要的时候!也就是按需分配,还做到了
:延时分配,因为当被创建的时候,不一定被立马调度,如果不立马被调度,那就不需要先给它
分配空间。延时分配可以保证系统可用资源是最大化的。
2.进程终止
一个进程退出有如下三种情况:
1.代码运行完毕,结果正确(逻辑正确,正常退出)
2.代码运行完毕,结果不正确(代码逻辑出现问题,结果不正确)
3.代码异常终止
如何查看进程的退出码:
echo $?可以查看进程的退出码,例如:
[LF@ecs-100710 lesson4]$ ls
makefile test test.c tt tt.c
[LF@ecs-100710 lesson4]$ cat tt.c
#include<stdio.h>
void fun()
{
printf("hello\n");
exit(-1);
}
int main()
{
int k=0;
fun();
printf("%d\n",k);
return 0;
}
[LF@ecs-100710 lesson4]$ ./tt
hello
[LF@ecs-100710 lesson4]$ echo $?
255
[LF@ecs-100710 lesson4]$
补充:
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值,
虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
return退出
异常终止:
1.使用ctr+c终止前台进程
2.使用kill命令发生终止信号
3.进程等待
为何要进行进程等待?
1.子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,kill -9 也无能杀死它,因为谁也没有办法杀死一个已经死去的进程。
2.父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
3.父进程通过进程等待的方式,获取子进程退出信息,回收子进程资源。
3.1进程等待的方法
3.1获取子进程的status
下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充。
如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。
status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只研究status低16比特位)
可以通过位操作获得退出码和退出信号,例如:
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
系统当中提供了两个宏来获取退出码和退出信号,例如:
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //获取退出码
注意:如果一个进程是被信号所杀,那么退出码便没有意义了。
3.1.阻塞等待
1.wait
2.waitpid
先看下wait,函数原型
pid_t wait(int* status);
返回值有两种情况:
1.等待成功返回等待进程的id, 2.等待失败返回-1。
参数:status是一个输出形参数。在c语言中如果我们想拿到函数里面的值我们需要传地址过去,在函数里面解引用从而达到能够在函数外面拿到函数里面的某些值。通过stauts我们可以获取等待进程的退出状态。当然如果不关心这个可以设置为NULL
举例:
使用一段脚本来监控进程: while :; do ps axj | head -1 && ps ajx | grep test; sleep 4; echo "#############################################"; done
#include<iostream>
using namespace std;
#include<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id=fork();
int k=0;
if(id==0)
{
while(1)
{
printf("i am a process,id=%d\n",getpid());
k++;
if(k==5)
{
printf("我要死了,请回收我\n");
exit(0);
}
sleep(1);
}
}
else
{
int status = 0;
sleep(8);
pid_t ret = wait(&status);
if(ret<0)
{
printf("等待失败\n");
}
else{
printf("等待成功,ret=%d\n",ret);
if(WIFEXITED(status)){
printf("exit code: %d\n",WEXITSTATUS(status));
}
}
sleep(5);
}
return 0;
}
结果:
2..waitpid
返回值:
1、等待成功返回被等待进程的pid。
2、如果设置了选项WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
参数:
1、pid:待等待子进程的pid,若设置为-1,则等待任意子进程。
2、status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
3、options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。设置为0时,则循环等待。
测试代码:一直等待
int main()
{
pid_t id=fork();
int k=0;
if(id==0)
{
while(1)
{
printf("i am a process,id=%d\n",getpid());
sleep(1);
}
}
else
{
int status=0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("wait success, ret : %d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n",
ret, (status>>8)&0xFF, status&0x7F);
}
printf("父进程做自己的事情了\n");
}
return 0;
}
结果:直到子进程退出(被杀死),父进程才会执行后面的代码
更改下代码:把子进程代码部分更改
while(1)
{
printf("i am a process,id=%d\n",getpid());
sleep(1);
if(k==5)
{
k%=0;
}
k++;
}
结果:
多进程创建以及等待的代码:
int main()
{
pid_t ids[10];
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
printf("child process created successfully...PID:%d\n", getpid());
sleep(3);
exit(i); // 将子进程的退出码设置为该子进程PID在数组ids中的下标
}
ids[i] = id;
}
for (int i = 0; i < 10; i++)
{
int status = 0;
cout << k << endl;
pid_t ret = waitpid(ids[i], &status, 0);
if (ret >= 0)
{
printf("wiat child success..PID:%d\n", ids[i]);
if (WIFEXITED(status))
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
else
{
printf("killed by signal %d\n", status & 0x7F);
}
}
}
return 0;
}
结果:
3.1.2 非阻塞等待
非阻塞等待:父进程不断检查子进程的退出状态,而在检测子进程的期间可以干自己的事情。
例如:
int main()
{
pid_t id=fork();
if(id==0)
{
int k=0;
while(k<5)
{
printf("i am a process,id=%d\n",getpid());
sleep(1);
k++;
}
}
else
{
while(true){
int status=0;
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret > 0)
{
printf("wait success, ret : %d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n",
ret, (status>>8)&0xFF, status&0x7F);
break;
}
else if(ret==0)
{
printf("father do other thing\n");
sleep(2);
}
else
{
printf("error\n");
break;
}
}
}
return 0;
}
结果:
可用kill -l命令查看信号: