linux第1天 fork exec 守护进程
概念方面
文件是对I/O设备的抽象表示、虚拟存储器是对主存和磁盘I/O设备的抽象表示、进程则是对处理器、主存和I/O设备的抽象表示
中断
早期是没有进程这个概念,当出现中断技术以后才出现进程这个概念
分式操作系统
基于时间片轮转
进程是操作系统对资源的一种抽象,一个进程:代码段、数据段、堆栈段、+进程控制块(PCB)
PCB是操作系统感知进程存在的一个重要数据结构(cpu通过进程控制块来控制进程)
fork()
使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,,进程PCB控制块。
子进程与父进程的区别在于:
1、父进程设置的锁,子进程不继承
2、各自的进程ID和父进程ID不同
3、子进程的未决告警被清除;
4、子进程的未决信号集设置为空集。
理解1:fork系统调用之后,父子进程将交替执行。
理解2:怎么样理解一次调用2次返回? 父子进程将在各自的进程空间里返回.
理解3:怎么样理解,fork返回值大于零的是父进程,为什么这样设计:?通过子进程获取父进程简单getppid(),而通过父进程获取子进程麻烦,还不如把子进程pid设置为0
理解4:怎么样理解分支在fork之后,而不是父进程main函数的开始?子进程继承父进程的.进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,,进程PCB控制块。
写时复制copy on write
如果多个进程要读取它们自己的那部分资源的副本,那么继承时复制是不必要的。
每个进程只要保存一个指向这个不需要写入数据的资源的指针就可以了。
如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源对应的页。这就是写时复制的含义
原因分析:加快速度,linux内核是段页式管理机制(因段管理从0开始,),也可叫页式管理机制。只复制对应的页。缺页,在中断查询,再赋值。
对父进程打开的文件,子进程也会继承过来.然后被打开的文件中,文件表中refcnt引用加1
对于信号方面,子进程继承阻塞信号设置,但要抛弃父进程的未决信号集
fork和vfork
1)在fork还没实现copy on write之前。Unix设计者很关心fork之后立刻执行exec所造成的地址空间浪费,所以引入了vfork系统调用。 2)vfork有个限制,子进程必须立刻执行_exit或者exec函数。 即使fork实现了copy on write,效率也没有vfork高,但是我们不推荐使用vfork,因为几乎每一个vfork的实现,都或多或少存在一定的问题。 |
结论: 1:fork子进程拷贝父进程的数据段 Vfork子进程与父进程共享数据段; 2:fork父、子进程的执行次序不确定 Vfork:子进程先运行,父进程后运行; |
exec函数族
execve替换进程映像(加载程序)注意execve是一个系统调用;
替换意味着:代码段、数据段、堆栈段、进程控制块PCB全部替换。
int main(void) { int fd; pid_t pid; signal(SIGCHLD, SIG_IGN);//不接收SIGCHILD信号,父进程关闭会子进程直接给1号进程管制 printf("befor fork pid:%d \n", getpid()); g_num = 10; pid = vfork();//不稳定,退出时可能会出现错误. if (pid == -1) { printf("pid < 0 err.\n"); return -1; } if (pid > 0) { printf("parent: pid:%d \n", getpid()); } else if (pid == 0) { printf("child: %d, parent: %d \n", getpid(), getppid()); char *const argv[] = {"ls", "-lt", NULL}; execve("/bin/ls", argv, NULL); //注意:此处有/bin/ls程序结束程序,不会出现coredump现象 printf("测试下面这句话还执行吗\n"); //exit(0); //1 vfork只有需要用exit(0) _exit(0) //2 测试return 0; 区别 } printf("fork after....\n"); return 0; }
进程终止的5种方式
正常退出
从main函数返回
调用exit 会清空缓冲区,再调用内核中的退出函数
调用_exit 直接调用内核中的退出函数
异常退出
调用abort 产生SIGABOUT信号 强制关闭
由信号终止 ctrl+c SIGINT 强制关闭
atexit可以注册终止处理程序,ANSI C规定最多可以注册32个终止处理程序,它其实也是一个回调函数,传入的是函数指针
exec关联的函数族
q 包含头文件<unistd.h>
q 功能用exec函数可以把当前进程替换为一个新进程。exec名下是由多个关联函数组成的一个完整系列,头文件<unistd.h>
q 原型
/*
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...); PATH
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[]);
以execlp、execvp、和execle讲解 path参数表示你要启动程序的名称包括路径名 arg参数表示启动程序所带的参数 返回值:成功返回0,失败返回-1 |
execl,execlp,execle(都带“l”)的参数个数是可变的,参数以一个空指针结束。 execv和execvp的第二个参数是一个字符串数组,新程序在启动时会把在argv数组中给定的参数传递到main 这些函数通常都是用execve实现的,这是一种约定俗成的做法,并不是非这样不可。 |
名字最后一个字母是“p”的函数会搜索PATH环境变量去查找新程序的可执行文件。如果可执行文件不在PATH定义的路径上,就必须把包括子目录在内的绝对文件名做为一个参数传递给这些函数 总结:l代表可变参数列表,p代表在path环境变量中搜索file文件。envp代表环境变量。 |
int main(void) { //演示程序被完全替换 //替换以后,pid不会发生变化 //注意 printf后的\n不能忘记,不然main函数打印不出来, //因为 \n 具有清除缓冲区的作用 printf(“main getpid: %d\n”, getpid()); //execlp(“ls”, “ls”, “-lt”, NULL); int ret = execlp(“./testpid2”, NULL, NULL); if (ret == -1) { perror(“ERR: “); } printf(“fork after….\n”); return 0; }
extern char **environ;//由内核分配内存,内存的第三种模型,
有关环境变量 int main(int argc, char *argv[]) { printf(“main egin…\n”); //1)如果envp环境参数列表不填写,则testpidandenv程序会使用默认的环境参数列表 //2)如果envp环境参数列表填写,则testpidandenv程序会使用你填写的环境参数列表 char *ars[] = {“aa=111”, “bb==222”, NULL}; int ret = execle(“./testpidandenv”,”testpidandenv”, NULL,NULL); //int ret = execle(“./testpidandenv”,”testpidandenv”, NULL,ars); if (ret == -1) { perror("main:"); } printf("main end...\n"); return 0; }
关于守护进程相关知识,顺便加强一下shell知识
当我们用telnet登陆linux服务器,验证好帐号密码之后,会建立一个会话期(session),同时服务器会运行一个shell,针对登陆上的帐户而言
此时,我们在这个终端上运行的所有程序命令都基于这个会话期,一旦终端关闭,运行于这个会话期的所有程序都会关闭,linux内核提供了一个函数setsid
脱离了终端,脱离了这个session,就像WINDOWS的服务程序一样,驻留在linux中,故名思议,守护进程.
注:调用setsid的不能是进程组长(一般为父进程),通常的做法是fork一个子进程,然后再调用setsid,
调用之后,需要改变当前路径为根路径,因为程序启动时,是与启动路径绑定的,如果不改变,将来要对启动路径作某些更改的时候,会失败.
另外还需要将标准输入,输出,错误文件描述符都重定项到"dev/null",避免资源浪费