四.初识linux进程

版权声明:本文为博主原创文章,允许转载请注明。谢谢! https://blog.csdn.net/wangweijundeqq/article/details/89603440

笔记地址:https://note.youdao.com/ynoteshare1/index.html?id=3719e28578c65407e8bb16b479491628&type=note

一、进程的开始和结束

E:\Linux\3.AppNet\4.process\4.1

2019/03/16 21:29

进程状态:运行、等待、停止、就绪、僵尸

1、main函数由谁调用

(1)编译链接时的引导代码。操作系统下的应用程序其实在main执行前也需要先执行一段引导代码才能去执行main,编译连接时(准确说是连接时)由链接器将编译器中事先准备好的引导代码给连接进去和我们的应用程序一起构成最终的可执行程序。

(2)运行时的加载器。加载器是操作系统中的程序,当我们去执行一个程序时(譬如./a.out,譬如代码中用exec族函数来运行)加载器负责将这个程序加载到内存中去执行这个程序。

(3)程序在编译连接时用链接器,运行时用加载器,这两个东西对程序运行原理非常重要。

(4)argc和argv的实现实现是由于信号机制

2、程序如何结束

(1)正常终止:return、exit、_exit

(2)非正常终止:自己或他人发信号终止进程

3、atexit注册进程终止处理函数

(1)实验演示

void func(void)

{

printf("hello No.2\n");

}

int main(void)

{

printf("hello No.1\n");

atexit(func);//调用atexith函数

printf("hello No.3\n");

return 0;

}

(2)atexit注册多个进程终止处理函数,先注册的后执行(先进后出,和栈一样)atexit 其实是一个回调函数(函数指针) 用于主函数执行结束后,调用指针指向的函数。比如如下程序,调用指针指向 func函数。

void func(void)

{

printf("hello No.2\n");

}

void func1(void)

{

printf("hello No.3\n");

}

int main(void)

{

printf("hello No.1\n");

atexit(func);

atexit(func1);

return 0;

}

(2)return、exit和_exit的区别:return和exit效果一样,都是会执行进程终止处理函数,但是用_exit终止进程时并不执行atexit注册的进程终止处理函数。

int main(void)

{

printf("hello No.1\n");

atexit(func);

atexit(func1);

_exit(0) ;

}

二、进程环境

1、环境变量

使用命令 export 查看环境变量 

在每个进程中都有一个环境变量表,所以我们可以在进程中直接使用环境变量 

注意“使用 环境变量,就注定和OS环境有关了,注意移植问题” 

使用环境变量的方法: 

environ

#include<stdio.h>

int main(int argc,char *argv[])

{

int i = 0;

extern char **environ; //声明即可以引用

while(NULL != environ[i])

{

printf("第%d个环境变量: %s\n",i,environ[i]);

i++;

}

printf("Code Access~\n");

return 0;

}

获取指定环境变量函数:getenv

2、进程运行的虚拟地址空间

【虚拟内存技术】

是Linux系统设计的技术,一个进程认为4G的内存空间中,只有自己和操作系统。

实际也许你的设备只有512M,那该怎么实现呢?

  • 虚拟地址映射。

与ucos不同,linux能将虚拟地址映射到真正的物理空间中,是一种强大的内存管理方式

  • 分布映射

1G的内存每次存放一部分,分步骤依次放入运行时内存

这样做的优势:

1、进程隔离,做到各个进程不使用同一段内存 ,安全高效。

2、提供多进程同时运行,便于设备的更新和维护。(旧手机不能更新系统就是这样的道理)

三、进程的正式引入

1、进程ID

getpid 获取进程id 

getppid 获取父进程id 

getuid 获取usrid 

getgid 获取groupid

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

int main(void)

{

pid_t p1=NULL;

pid_t p2=NULL;

p1=getpid();

p2=getppid();

printf("PID=%d\n",p1);

printf("Parent id=%d\n",p2);

return 0;

}

2、多进程调度原理

多进程调度原理:宏观的并行,微观的串行。 

实际现在OS的最小调度单位是线程,普遍认为是进程。

3、新进程诞生的方法(fork的引入)

  • fork的内部原理

linux中的设计:新进程由老进程复制得出。复制之后要改pid等参数

新进程就是子进程,老进程就是父进程。

fork函数调用一次会返回2次,等于0的是子进程,大于0的就是父进程。

当fork一返回,main函数在操作系统中就变成2份,而且轮番运行。

那么比如打印helloworld,父进程打印helloworld,子进程也在helloworld。

如果想限定某一个进程中执行某一段代码,那么就用if来判断返回值即可

int main(void)

{

pid_t p1=-1;

p1=fork();

if (p1 == 0)

{

//子进程操作

printf("子进程,pid=%d.\n",getpid());

printf("子进程,pid=%d.\n",getpid());

}

if (p1 > 0)

{

//父进程操作

printf("父进程,pid=%d.\n",getpid());

}

if (p1 < 0)

{

//这里一定是fork出错了 

}

//在这里所做的操作,在父子进程都有这个操作

printf("helloworld,pid=%d\n",getpid());

return 0;

return 0;

}

打印出两个helloworld,分别是父进程和子进程的。

四、父子进程对文件的操作

1、子进程继承父进程中打开的文件

子进程和父进程的文件指针是彼关联的,很像是O_APPEND标志后的位置 

但实际测试时,有的时候会看到一个,有点像分别写,但是实际上不是。

open("xxx", O_RDWR | O_TRUNC)

pid = fork();

if (pid == 0)

打印子进程操作11111

if (pid > 0)

打印父进程操作22222

出现的结果有4种

11111

2222 2

1111122222 (理想状况)

2222211111

这个结果的成因是因为我们 的 程序太简短了,有可能open了之后,父进程写完就返回了 ,return之前他会关闭掉我们的fd文件描述符,导致子进程还没来得及写,文件就被关闭了。

避免这种情况的方法是加入sleep(x)函数来睡眠x秒,

// 首先打开一个文件

int fd = -1;

pid_t pid = -1;

fd = open("1.txt", O_RDWR | O_TRUNC);

if (fd < 0)

{

perror("open");

return -1;

}

// fork创建子进程

pid = fork();

if (pid > 0)

{

// 父进程中

printf("11111.\n");

write(fd, "11111", 5);

sleep(1);

}

else if (pid == 0)

{

// 子进程

printf("22222.\n");

write(fd, "22222", 5);

sleep(1);

}

else

{

perror("fork");

exit(-1);

}

close(fd);

2、父子进程各自独立打开同一文件实现共享

  • 思路:

if (pid == 0) 

子进程操作,打开xxx,写1111 

if (pid > 0) 

父进程操作,打开xxx,写2222

最后的结果只有2222,也就是分别写。

// 首先打开一个文件

int fd = -1;

pid_t pid = -1;

// fork创建子进程

pid = fork();

if (pid > 0)

{

// 父进程中

fd = open("1.txt", O_RDWR );

if (fd < 0)

{

perror("open");

return -1;

}

printf("11111.\n");

write(fd, "11111", 5);

sleep(1);

}

else if (pid == 0)

{

// 子进程

fd = open("1.txt", O_RDWR );

if (fd < 0)

{

perror("open");

return -1;

}

printf("22222.\n");

write(fd, "22222", 5);

sleep(1);

}

else

{

perror("fork");

exit(-1);

}

close(fd);

  • 如果将以上代码open时使用O_APPEND标志??

fd = open("1.txt", O_RDWR | O_APPEND);

实际测试结果标明O_APPEND标志可以把父子进程各自独立打开的fd的文件指针给关联起来,即接续写,实现分别写。

子进程的最终目的:要独立运行去执行另外的程序。

五、进程的诞生和消亡

1、进程的诞生

进程0 系统态,内核启动的时候手动构成的 

进程1 系统态,由进程0 ,fork产生 

进程2 用户态,以后的每个进程都是fork诞生的

2、进程的消亡

进程在运行时需要消耗系统资源(内存、IO),进程终止时理应完全释放这些资源(如果进程消亡后仍然没有释放相应资源则这些资源就丢失了)

例如:IO丢失 产生.swp文件而且不删是打不开的

内存丢失:内存泄漏 

①linux的设计:

  • 每一个进程退出时,操作系统会自动回收这个进程涉及到的所有的资源(譬如malloc申请的内容没有free时,当前进程结束时这个内存会被释放,譬如open打开的文件没有close的在程序终止时也会被关闭)。但是操作系统只是回收了这个进程工作时消耗的内存和IO,而并没有回收这个进程本身占用的内存(8KB,主要是task_struct和栈内存)。这里,就引出了父进程,专业收尸。
  • 父进程 给子进程收尸,每一个进程都有自己的父进程,一直往上一直往上,直到回收资源到进程0,这个时候就关机了。所以一个进程是不能没有父进程的,没有父进程就没法回收资源了

3、僵尸进程

子进程先于父进程结束 //进程退出,回收资源,但进程本身的栈和描述结构体(task_struct)未回收

(而且父进程尚未收尸) //这两段8KB不是OS带来的,也不是OS能回收的,如果收尸就需要父进程 。

  • 收尸方法: 

1、父进程可以使用wait()或waitpid回收子进程资源,并且获取子进程退出状态。 

2、父进程结束时,会自动回收子进程僵尸资源。父进程也可以不使用wait或者waitpid回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源(为了防止忘记调用wait/waitpid,防止内存泄漏)

4、孤儿进程

  • 父进程先于子进程结束。 子进程还没变僵尸,父进程先死亡了。
  • 子进程还没回收,父进程先死了,子进程成为孤儿进程
  • Linux规定:所有的孤儿进程都成为一个特殊进程(进程1,也就是init)
  • 如果进程1死了也就关机了。
  • 注意:ubuntu系统不是进程1,查询父进程的话可能不是1,但肯定是inti –user。

六、父进程wait回收子进程

1、wait的工作原理

(1)子进程结束时,系统向其父进程发送SIGCHILD信号

(2)父进程调用wait函数后阻塞

(3)父进程被SIGCHILD信号唤醒然后去回收僵尸子进程

(4)父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。

(5)若父进程没有任何子进程则wait返回错误

2、用法:  

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

参数:  

pid:

小于-1 //意味着等待所有其进程组标识等于pid绝对值的子进程。

 -1 //意味着等待任何子进程。

 0 //意味着等待任何其组标识等于调用进程组标识的进程。

大于0 //意味着等待其进程标识等于pid的进程。

 

options:可以是0个或多个以下符号常量通过or运算的组合体

WNOHANG //非阻塞式:

WUNTRACED //如果有处于停止状态的进程将导致调用返回。

WCONTINUED //如果停止了的进程由于SIGCONT信号的到来而继续运行,调用将返回。

3、wait实战编程

E:\Linux\3.AppNet\4.process\4.6

#include <unistd.h>

#include <sys/types.h>

#include <sys/wait.h>

#include<stdio.h>

#include<stdlib.h>

int main(int argc,char *argv[])

{

pid_t ret = -1;

pid_t pid = -1;

int status = -1; //

pid = fork(); //创建子进程,返回两个pid,一个是父进程pid=0,一个是子进程pid>0

if(pid == 0)

{

//子进程

printf("child ready\n");

printf("child pid = %d\n",getpid());

exit(1);

}

else if(pid > 0)

{

//父进程

printf("parent ready\n");

ret = wait(&status); //wait是阻塞式的,不用sleep

//status:提供给操作系统一个用户态指针,然后由内核来填充这个值

printf("回收子进程,子进程pid=%d\n",ret);

//printf("是否正常退出:%d\n",WIFEXITED(status) );

}

else

{

//错误

perror("fork");

}

printf("Code Access~\n");

return 0;

}

parent ready —父进程先执行!由于wait产生阻塞,等子进程结束信号再回收

child ready —子进程执行

child pid = 6426

Code Access~ —子进程godie,系统发送SIGCHILD给父进程

回收子进程,子进程pid=6426—父进程回收子进程

Code Access~ —父进程godie

4、3个宏

WIFEXITED、WIFSIGNALED、WEXITSTATUS这几个宏用来获取子进程的退出状态。

WIFEXITED宏用来判断子进程是否正常终止(return、exit、_exit退出)

WIFSIGNALED宏用来判断子进程是否非正常终止(被信号所终止)

WEXITSTATUS宏用来得到正常终止情况下的进程返回值的。

使用方法:printf("是否正常退出:%d\n",WIFEXITED(status) );

七、waitpid函数介绍

E:\Linux\3.AppNet\4.process\4.7

1、waitpid和wait差别

(1)基本功能一样,都是用来回收子进程

(2)waitpid可以回收指定PID的子进程

(3)waitpid可以阻塞式或非阻塞式两种工作模式

2、waitpid原型在上面已有介绍

3、代码实例(代码和上面wait类似)

(1)使用waitpid实现wait的效果

ret = waitpid(-1, &status, 0);

-1表示不等待某个特定PID的子进程而是回收任意一个子进程,0表示用默认的方式(阻塞式)来进行等待,返回值ret是本次回收的子进程的PID

(2)ret = waitpid(pid, &status, 0);

等待回收PID为pid的这个子进程,如果当前进程并没有一个ID号为pid的子进程,则返回值为-1;如果成功回收了pid这个子进程则返回值为回收的进程的PID

(3)ret = waitpid(pid, &status, WNOHANG);

这种表示父进程要非阻塞式的回收子进程。此时如果父进程执行waitpid时子进程已经先结束等待回收则waitpid直接回收成功,返回值是回收的子进程的PID;如果父进程waitpid时子进程尚未结束则父进程立刻返回(非阻塞),但是返回值为0(表示回收不成功)。

4、竟态初步引入

(1)竟态全称是:竞争状态,多进程环境下,多个进程同时抢占系统资源(内存、CPU、文件IO)

(2)对操作系统来说是危险的。容易造成无法预料的结果。显而易见竞争状态是有害的,操作系统对竞争状态的出现设计了一些应对的方法,包括sleep也是一种消灭竟态的方法,同步机制也是如此。

猜你喜欢

转载自blog.csdn.net/wangweijundeqq/article/details/89603440
今日推荐