程序的运行过程
1.程序是如何运行起来的
  (1)在内存中划出一片内存空间
  (2)将硬盘上可执行文件中的代码(机器指令)拷贝到会出的内存空间空间中
  (3)pc指向第一条指令,cpu取指运行
    当有os时,以上过程都是通过调用相应的api来实现的
    在linux下,os提供2个非常关键的api,fork和exec

  fork:开辟出一块内存空间
  exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,cpu开始运行,进程就运行起来了,
  运行起来的进程会与其它的进程切换着并发运行

fork
#include <unistd.h>
pid_t fork(void);
(1)功能:从调用该函数的进程复制出子进程,被复制的被称为父进程,复制出来的进程称为子进程
复制后有2个结果
  1)依照父进程内存空间的样子,原样复制地开辟出子进程的内存空间
  2)由于子进程的空间是原样复制的父进程空间,因此子进程内存空间中的代码和数据和父进程的完全相同
    其实复制父进程的主要目的,就是为了复制出一块内存空间
    只不过复制的附带效果是,子进程原样的拷贝了一份父进程的代码和数据,事实上复制出子进程内存空间的主要目的其实是为了exec加载新的程序代码

(2)返回值
  由于子进程原样复制了父进程代码,因此父子进程都会执行fork函数(暂定)
  1)父进程的fork,成返回子进程的Pid,失败返回-1
  2)子进程的fork,成功返回0 ,失败-1

#include <stdio.h>
main(){
    int ret_from_fork;
    printf("Before :my pid is %d \n   ",getpid() ); //加\n和不加的区别(子进程复制父进程的数据 缓存)
    ret_from_fork=fork();
    if(ret_from_fork >0){
        printf("parent_pid:%5d,ret_from_fork=%5d\n",getpid(),ret_from_fork);
    }else if(ret_from_fork ==0 ){
        printf("child_pid:%5d,ret_from_fork=%5d\n",getpid(),ret_from_fork);
    }
    printf("pid=%d %s\n",getpid(),"end");
    while (1);
}

复制原理:
    linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是os通过数据结构基于物理内模拟出来的,因此底层对应的还是物理内存
    复制子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,
    相应的底层会对应着一片新的物理内存空间,里面放了与父进程一模一样的代码和数据
父子进程各自执行哪些代码
(1)父进程
  1)执行fork前的代码
  2)执行fork函数
    父进程执行fork函数时,调用成功会返回值为子进程的PID,进入if(ret > 0){}中,执行里面的代码。
    if(ret > 0){}中的代码只有父进程才会执行
  3)执行fork函数后的代码
(2)子进程
  1)fork前的代码
  尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行。
  2)子进程调用fork时,返回值为0,注意0不是PID。
    进入if(ret == 0){},执行里面的代码
    if(ret == 0){}中的代码只有子进程执行

  3)执行fork后的代码

2.3 父子进程共享操作文件
(1)情况1:独立打开文件
  多个进程独立打开同一文件实现共享操作
  1)代码 fork_file.c
    

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define F "./file.txt"

int main(int argc, char const *argv[])
{
    pid_t ret = 0;
    int fd=0;
    char msg[20]={0};
    ret = fork();
    if(ret >0){
        sprintf(msg,"parent pid %d\n",getpid());
        fd=open(F,O_RDWR|O_CREAT|O_APPEND,0644);
        write(fd,msg,strlen(msg));
    }else if(ret == 0){
        sprintf(msg,"child pid %d\n",getpid());
        fd=open(F,O_RDWR|O_CREAT|O_APPEND,0644);
        write(fd,msg,strlen(msg));
    }
    return 0;
}

  独立打开同一文件时,父子进程各自的文件描述符,指向的是不同的文件表。
  因为拥有不同的文件表,所以他们拥有各自独立的文件读写位置,会出现相互覆盖情况,如果不想相互覆盖,
  需要加O_APPEND标志
(2)情况2:fork之前打开文件
  1)fork_file1.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define F "./file.txt"

int main(int argc, char const *argv[])
{
    pid_t ret = 0;
    int fd=0;
    char msg[20]={0};
    fd=open(F,O_RDWR|O_CREAT,0644);
    ret = fork();
    if(ret >0){
        sprintf(msg,"parent pid %d\n",getpid());
        write(fd,msg,strlen(msg));
    }else if(ret == 0){
        sprintf(msg,"child pid %d\n",getpid());
        write(fd,msg,strlen(msg));
    }
    return 0;
}

  2)文件表结构

    子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件

    像这种继承的情况,父子进程这两个相同的“文件描述符”指向的是相同的“文件表”
    由于共享的是相同的文件表,所以拥有共同的文件读写位置,不会出现覆盖的情况。

子进程的0 1 2这三个打开的文件描述符,其实也是从父进程那里继承过来的,并不是子进程自己去打开的,同样的父进程的
0 1 2又是从它的父进程那里继承过来的,最根溯源的话,都是从最原始的进程哪里继承过来的,最原始的进
程是init进程。

init进程会去打开标准输入,标注输出、标准出错输出这三个文件,然后0 1 2分别指向打开的文件,之后所有进程的0 1 2,
实际上都是从最开始的init进程那里继承而来的

  12-程序的运行和fork,execv,system_linux网络编程

 

2.4 子进程会继承父进程的哪些属性
2.4.1 子进程继承如下性质
  (1)用户ID,用户组ID
  (2)进程组ID
  (3)会话期ID
  (4)控制终端
  (5)当前工作目录
  (6)根目录
  (7)文件创建方式屏蔽字(umask修改)
  (8)环境变量
  (9)打开的文件描述符
    等等

2.4.2 子进程独立的属性
  (1)进程ID
  (2)不同的父进程ID
  (3)父进程设置的锁,子进程不能被继承
    等等

3. exec加载器
  exec加载器就是加载函数
3.1 exec的作用
父进程fork复制出子进程的内存空间后,子进程内存空间的代码和数据和父进程是相同的,这样没有太大的意义,我们需要在子进
程空间里面运行全新的代码,这样才有意义。
怎么运行新代码?
  我们可以在if(ret==0){}里面直接写新代码,但是这样子很麻烦,如果新代码有上万行甚至更多的话,这种做法显然是不行的,因此
  就有了exec加载器
  有了exec后,我们可以单独的另写一个程序,将其编译好后,使用exec来加载即可
3.2 exec函数族
  exec的函数有很多个,它们分别是execve、execl、execv、execle、execlp、execvp,都是加载函数。
  其中execve是系统函数,其它的execl、execv、execle、execlp、execvp都是基于execve封装得到的库函数
  execve函数,这个函数懂了,其它的函数原理是一样的
3.2.1 execve函数原型
#include <unistd.h>
int execve(const char *filename, char **const argv, char **const envp);
(1)功能:向子进程空间加载新程序代码(编译后的机器指令)。
(2)参数:
1)filename:新程序(可执行文件)所在的路径名
  可以是任何编译型语言所写的程序,比如可以是c、c++、汇编等,这些语言所写的程序被编译为机器指令后,
  都可以被execve这函数加载执行。
  正是由于这一点特性,我们才能够在C语言所实现的OS上,运行任何一种编译型语言所编写的程序。
  疑问:java可以吗?
  java属于解释性语言,它所写的程序被编译后只是字节码,并不是能被CPU直接执行的机器指令,所以不能被execve直接加
  载执行,而是被虚拟机解释执行。
  execve需要先加载运行java虚拟机程序,然后再由虚拟机程序去将字节码解释为机器指令,再有cpu去执行,在后面还会详细
  讨论这个问题
2)argv:传给main函数的参数,比如我可以将命令行参数传过去
3)envp:环境变量表

(3)返回值:函数调用成功不返回,失败则返回-1,且errno被设置

pro.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[],char **environ)
{
    int i = 0;
    printf("argv:\n");
    for(i=0;i<argc;i++){
        printf("%s\n",argv[i] );
    }
    printf("--------------------\nenviron:\n");

    for(i=0 ;NULL != environ[i];i++){
        printf("%s\n",environ[i] );
    }
    printf("\n-----end-------\n");
    return 0;
}

main.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    pid_t ret = 0;
    
    ret = fork();
    if(ret >0){
        sleep(2);
    }else if(ret == 0){
        extern char **environ;
        execv("./pro",argv,environ);
    }
    return 0;
}

12-程序的运行和fork,execv,system_linux网络编程_02

 

exec的作用:将新程序代码加载(拷贝)到子进程的内存空间,替换掉原有的与父进程一模一样的代码和数据,让子进程空间运行全
新的程序

 

3.3 在命令行执行./a.out,程序是如何运行起来的
(1)窗口进程先fork出子进程空间
(2)调用exec函数加载./a.out程序,并把命令行参数和环境变量表传递给新程序的main函数的形参

3.4 双击快捷图标,程序是怎么运行起来的
(1)图形界面进程fork出子进程空间
(2)调用exec函数,加载快捷图标所指向程序的代码
以图形界面方式运行时,就没有命令行参数了,但是会传递环境变量表

4. system函数

如果我们需要创建一个进子进程,让子进程运行另一个程序的话,可以自己fork、execve来实现,但是这样的操作很麻烦,
所以就有了system这个库函数,这函数封装了fork和execve函数,调用时会自动的创建子进程空间,并把新程序的代码加载到
子进程空间中,然后运行起来。

虽然有system这函数,但是我们还是单独的介绍了fork和execve函数,因为希望通过这两个函数的介绍,让大家理解当有OS支持时,
程序时如何运行起来的。

4.1 system函数原型
#include <stdlib.h>

int system(const char *command);

(1)功能:创建子进程,并加载新程序到子进程空间,运行起来

(2)参数:新程序的路径名

(3)代码演示
  system("ls");
  system("ls -al");
5. 回收进程资源
  进程运行终止后,不管进程是正常终止还是异常终止的,必须回收进程所占用的资源
5.1 为什么要回收进程的资源?
(1)程序代码在内存中动态运行起来后,才有了进程,进程既然结束了,就需要将代码占用的内存空间让出来(释放)。
(2)OS为了管理进程,为每个进程在内存中开辟了一个task_stuct结构体变量,进程结束了,那么这个结构体所占用的内存空间也需要被释放
(3)等其它资源
5.2 由谁来回收进程资源
由父进程来回收,父进程运行结束时,会负责释放子进程资源
5.3 僵尸进程和孤儿进程
5.3.1 僵尸进程
  子进程终止了,但是父进程还活着,父进程在没有回收子进程资源之前,子进程就是僵尸进程。
  为什么子进程会变成僵尸进程?
  子进程已经终止不再运行,但是父进程还在运行,它没有释放子进程占用的资源,所以就变成了占着资源不拉屎僵尸进程
  就好比人死后不腐烂,身体占用的资源得不到回收是一样的,像这种情况就是所谓的僵尸
5.3.2 孤儿进程
  没爹没妈的孩子就是孤儿,子进程活着,但是父进程终止了,子进程就是孤儿进程
  为了能够回收孤进程终止后的资源,孤儿进程会被托管给我们前面介绍的pid==1的init进程,每当被托管的子进程终止时,init会立即
  主动回收孤儿进程资源,回收资源的速度很快,所以孤儿进程没有变成僵尸进程的机会
5.3.3 演示
(1)僵尸进程
ps查看到的进程状态
R 正在运行
S 处于休眠状态
Z 僵尸进程,进程运行完了,等待被回收资源
(2)孤儿进程

#include <stdio.h>
#include <unistd.h>

void parent_code(int delay){
    sleep(delay);
}
main(){
    pid_t pid;
    int status;

    pid=fork();
    //僵尸
    /*if(pid == 0){

    }
    if(pid>0){
        parent_code(20);
    }*/
    //孤儿 父进程先退出 子进程会被pid=1的进程收养
    if(pid == 0){
        int i=0 ;
        for(;i<10;i++){
            printf("pid=%d,ppid=%d\n",getpid(),getppid());
            if(getppid() == 1){
                printf("over \n");
                break;
            }
            sleep(1);
        }
        
    }
    if(pid>0){
        sleep(5);
        printf("parent exit ,pid=%d\n",getpid());
    }

}

6. wait函数
作用:父进程调用这个函数的功能有两个
(1)主动获取子进程的“进程终止状态”
(2)主动回收子进程终止后所占用的资源。
wait函数,在实际开发中用的很少,但是我们这里还是要介绍这个函数,因为如果你理解了这个函数,你才能理解进程
return/exit/_exit所返回的返回值,到底返回给了谁。
6.1 进程的终止
6.1.1 正常终止
(1)main调用return
(2)任意位置调用exit
(3)任意位置调用_exit
不管哪种方式来正常终止,最终都是通过_exit返回到OS内核的。
6.1.2 异常终止
如果是被某个信号终止的,就是异常终止
(1)自杀:自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死。
(2)他杀:由别人发一个信号,将其杀死
6.1.3 进程终止状态
(1)退出状态与"进程终止状态"
  之前将return、exit、_exit的返回值称为"进程终止状态",严格来说应该叫"退出状态",
  return(退出状态)、exit(退出状态)或_exit(退出状态)
  当退出状态被_exit函数交给OS内核,OS对其进行加工之后得到的才是"进程终止状态",父进程调用wait函数便可以
  得到这个"进程终止状态"

(2)OS是怎么加工的?
1)正常终止
  进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位
  不管return、exit、_exit返回的返回值有多大,只有低8位有效,所以如果返回值太大,只取低8位的值。
  int main(){
    return 1000;
  }
返回给shell的是232 ,1000的二进制 11 1110 1000 只取低8位 1110 1000

2)异常终止
  进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号

(3)父进程调用wait函数,得到"进程终止状态"有什么用
  父进程得到进程终止状态后,就可以判断子进程终止的原因是什么,如果是正常终止的,可以提取出返回值,如果是异常终止的,
  可以提取出异常终止进程的信号编号。

当有OS支持时,进程return、exit、_exit正常终止时,所返回的返回值(退出状态),最终通过
"进程终止状态"返回给了父进程

这有什么用,比如,父进程可以根据子进程的终止状态来判断子进程的终止原因,返回值等等,以决定是否重新启动子进程,
或则做一些其它的操作,不过一般来说,子进程的终止状态对父进程并没有太大意义

6.2 父进程如何从内核获取子终止状态
父进程可以获取,也可以不获取,父进程可以根据自己的具体需要来定。
6.2.1 如何获取
(1)父进程调用wait等子进程结束,如果子进程没有结束的话,父进程调用wait时会一直休眠的等(或者说阻塞的等)
(2)子进程终止返回内核,内核构建"进程终止状态"
  1)如果,子进程是调用return、exit、_exit正常终止的,将退出状态返回给内核后,内核会通过如下表达式构建"进程终止状态"
    进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位
  2)子进程是被某个信号异常终止的,内核会使用如下表达式构建"进程终止状态"
    进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号
(3)内核向父进程发送SIGCHLD信号,通知父进程子进程结束了,你可以获取子进程的"进程终止状态"了。
如果父进程没有调用wait函数的话,会忽略这个信号,表示不关心子进程的“进程终止状态”。
如果父进程正在调用wait函数等带子进程的“进程终止状态”的话,wait会被SIGCHLD信号唤醒,并获取进"进程终止状态"

一般情况下,父进程都不关心子进程的终止状态是什么,所以我们经常看到的情况是,不管子进程返回什么返回值,其实都无
所谓,因为父进程不关心。

不过如果我们的程序是一个多进程的程序,而且父进程有获取子进程"终止状态"的需求,此时我们就可以使用wait函数来获取了

6.2.2 wait函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
(1)功能:获取子进程的终止状态,主动释放子进程占用的资源
(2)参数:用于存放"进程终止状态"的缓存
(3)返回值:成功返回子进程的PID,失败返回-1,errno被设置

6.2.3 从进程终止状态中提取进程终止的原因、返回值或者信号编号
(1)进程状态中所包含的信息
  1)正常终止
    进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位

  2)异常终止
    进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号

(2)如何提取里面的信息
  系统提供了相应的带参宏,使用这个带参宏就可以从“进程终止状态”中提取出我们要的信息。
  提取原理:相应屏蔽字&进程终止状态,屏蔽掉不需要的内容,留下的就是你要的信息。
  哪里能查到这些带参宏,man查案wait的函数手册,即可看到

  1)WIFEXITED(status):提取出终止原因,判断是否是正常终止
  (a)如果表达式为真:表示进程是正常终止的
    此时使用WEXITSTATUS(status),就可以从里面提取出return/exit/_exit返回的"退出状态"
  (b)为假:不是正常终止的

  2)WIFSIGNALED(status):提取出终止原因,判断是否是被信号杀死的(异常终止)
  (a)如果表达式为真:是异常终止的
    此时使用WTERMSIG(status),就可以从里面提取出终止该进程的信号编号。
  (b)为假:不是异常终止的

wait.c

 

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern char **environ;

int main(int argc, char const *argv[])
{
    pid_t ret = 0;
    ret = fork();
    if(ret >0){
        printf("parent pid=%d\n", getpid());
        int status=0;
        wait(&status);//阻塞等待子进程死
        printf("status=%d\n",status); // 1->256
        if(WIFEXITED(status)){
            printf("exited :%d\n", WEXITSTATUS(status));
        }else if(WIFSIGNALED(status)){
            printf("signal killed:%d\n",WTERMSIG(status) );
        }
    }else if(ret == 0){
        printf("son pid=%d\n", getpid());
        execv("./wait_return",argv,environ); 
    }
    return 0;
}

 

 

 

wait_return.c

#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>


int main(int argc, char const *argv[])
{
    int i=0;
    while(i<20){
        printf("i=%d\n",i );
        sleep(1);
        i++;
    }
    return 1;
}

 


gcc wait_return.c -o wait_return
gcc wait.c
./a.out


自然结束
status=256
exited :1

和Kill -9 子进程
status=9
signal killed:9

 

通过判断终止原因、返回值、信号编号,父进程可以决定是否重新运行子进程,不过99%的情况是,父进程不关心子进程是怎么
终止的,它的返回值是什么,返回给了谁
(4)wait的缺点
  如果父进程fork创建出了好多子进程,wait只能获取最先终止的那个子进程的“终止”状态,其它的将无法获取,如果你想
  获取所有子进程终止状态,或者只想获取指定子进程的进程终止状态,需要使用wait的兄弟函数waitpid,它们的原理是相似的,

6. 进程状态
每个进程与其它进程并发运行时,该进程会在不同的“进程状态”之间进行转换

1)12-程序的运行和fork,execv,system_linux网络编程_03
2)进程运行状态有哪些
  (a)就绪态:表示进程准备就绪,随时等待被调度运行
  (b)执行态:被PID=0的调用进程调度,开始占有CPU,被cpu执行,运行的时间片到后让出CPU,再次进入就绪态,
    然后PID==0的调度进程开始调度其它进程,CPU去执行其他进程的指令
  (c)阻塞态:当进程需要的某些条件的不满足,进程会进入阻塞态(休眠),比如scanf,wait
    当条件满足后,会再次进入就绪态,等待再次被调度执行
  (d)终止态:进程终止(正常终止、异常终止)
    如果进程终止时,父进程没有回收进程资源的话,终止的进程会变成僵尸进程。

7. java进程
  java属于解释型语言,类似的像c#,python等都是属于解释型语言,而c/c++等则是属于编译型语言
7.1 编译型语言
  将编译型语言的程序编译后,得到的直接就是机器指令,可以被CPU直接执行
7.2 解释型语言
  将解释型语言的程序编译后,得到的只是字节码,字节码并不是机器指令,并不能直接被cpu执行,只有当字节码被虚拟机程序解
  释(翻译)为机器指令后才能被cpu执行,解释的过程也被称为翻译的过程
7.3 如何运行编译型和解释型语言的程序
  我们这里讨论的都是有OS支持的情况
7.3.1 运行编译型语言的程序
  (1)父进程(命令行终端窗口、图形界面)会fork复制出子进程空间
  (2)调用exec加载器,直接将编译后代码拷贝到子进程空间
  然后被CPU执行时,整个程序就运行起来了
  在程序里面,我们自己也可以调用fork和exec函数来执行另外的新程序

7.3.2 运行解释型语言的程序
(1)java程序的运行

  1)父进程(命令行窗口、图形界面)会fork复制出子进程空间
  2)调用exec加载java虚拟机程序,将虚拟机程序的代码拷贝到子进程空间中
  当java虚拟机程序运行起来后,会自动的去解释编译得到的java字节码文件,将字节码翻译为机器指令,cpu再去执行
  翻译得到的机器指令。
  每解释一句,cpu就会执行一句,在虚拟机的翻译下,整个java进程就开始运行起来了
  其实最简单的理解就是,java虚拟机就代表了java进程。
  当你运行另一个java程序时,又会自动地启动一个虚拟机程序来解释java字节码,此时另一个java进程又诞生了。
  也就是说你执行多少个java进程,就会运行多少个java虚拟机,当然java虚拟机程序在硬盘上只有一份,只不过被多次
  启动而已


(2)java虚拟机怎么得到
  运行解释型语言的程序时,必须要有虚拟机,不过好在当我们安装java等解释型语言的开发环境时,往往都会自
  动安装虚拟机程序,我们不用操心。
  当我们运行java程序时,虚拟机会被自动启动
  虚拟机一般是运行在OS上的,不过其实虚拟机也可以运行在没有OS的裸机上,当虚拟机安装在裸机上后,java程序也可以
运行在裸机上
java字节码文件
虚拟机
os
硬件


java字节码文件
虚拟机
硬件

(3)在java程序里面,也可以调用java库提供的类似的fork和exec函数,我们自己来创建一个java子进程,并执行新程序。
java库提供的类似的fork、exec函数,下层也是调用OS的fork、exec函数

(3)思考:虚拟机程序是什么语言写的
虚拟机程序能够被exec直接加载运行,说明虚拟机必然是编译型语言写的,如果虚拟机使用解释性语言来写的话,
编译后得到是字节码,字节码不是机器指令,此时还要另外安装一个程序来解释虚拟机程序,这就陷入了一个死循环
所以java虚拟机必须使用编译型语言来写,比如使用c/c++编写,一般来说是c写的。


8. 有关程序多进程

8.1 多进程 和 多线程

8.1.1 多进程

(1)程序多进程的例子:同时读鼠标和键盘

当鼠标和键盘没有数据时,这两个读操作会阻塞,如果在一个进程即读键盘、也读鼠标的话,就会出现相互堵的情况。

while(1)
{
read(键盘);
read(鼠标);
}
此时就可以fork创建出一个子进程,然后让父子进程并发的读数据。

父进程 子进程
read(键盘) read(鼠标)
父子进程并发运行,各自读各自的,互不干扰,这样就不会出现相互堵的情况了


8.1.2 多线程并发

在平时的开发中,小型的程序一般都是单进程的,此时很少会使用多进程来并发做事,因为多进程太过耗费计算机资源,为什么
多进程会如此的耗费计算机资源,我们后面讲到线程时再来详说。

如果程序真的需要并发的话,我们会在进程内部创建多个线程,使用多个线程来实现并发,比如:

主线程 次线程
read(键盘) read(鼠标)


后面课程会详细的介绍C线程函数。

疑问:程序多进程会不会用到?
答:当然会


8.2 什么情况下,我们让程序创建出多进程,实现多进程的并发
在一般小型应用程序的开发中,程序基本都是单进程的,如果涉及到并发时,往往会多线程来实现,但这并不是说
程序就不存在多进程的情况了,那么在什么情况下,程序会用到多进程呢?
比如:
(1)编写框架
安卓的系统框架,中大型游戏底层框架等,软件框架属于结构性的代码,在实现框架结构的过程中,基本都要使用多进程来搭
建这个框架。像框架这种东西,在实际开发中,并不是任何人都会涉及到的,对于绝大多数人来说,在实际开发中,基本只涉及到框架的使用,
但不涉及到框架的编写。
(2)如果你的程序必须启动新程序
1)例子1
比如windows图形界面、命令行,当我们在图形界面或者命令启动新程序时,必须创建子进程,然后在子进程空间中运
行新代码,这个时候必须要涉及多进程。

2)例子2
那些大型的业务功能复杂的软件,基本都是多进程的,为什么?
这里说的大型软件,有可能是客户端软件,也有可能是服务器端的软件。

对于大型的软件程序来说,往往包含很多种功能(功能套件),而且每一个功能具有相当的独立性,所以大型复杂的软件
在实现时,如果将这些独立功能全都放到单进程里面来实现,很不现实,因为所有的东西都杂糅在一起,很不好实现。

既然每个功能具有独立性,在大型软件里面,每个独立的功能,均由独立的进程来实现,其中有一个是主进程,主进程往往会
提供主的交互界面,主进程启动起来后,会去启动不同的子进程,每个子进程负责实现不同的功能。

在每个子进程的内部往往还会有多线程的参与,所以对于大型软件来说,多进程 和 多线程都会涉及

12-程序的运行和fork,execv,system_linux网络编程_04

(3)早期服务器程,会通过多进程来支持多用户的访问

12-程序的运行和fork,execv,system_linux网络编程_05

8. 进程关系
  进程间的关系,大致有三种,即父子关系、进程组关系、会话期关系。
8.1 父子关系
  已有进程调用fork创建出一个新的进程,那么这两个进程之间就是父子进程关系,子进程会继承和父进程的属性
8.2 进程组
8.2.1 什么是进程组
  多个进程可以在一起组成一个进程组,其中某个进程会担任组长,组长进程的pid就是整个进程组的组ID。
8.2.2 进程组的生命周期
  就算进程组的组长终止了,只要进程中还有一个进程存在,这个进程组就存在。
  进程组从开始被创建,到进程组最后一个进程结束,这段时间就是进程组的生命周期。
8.2.3 进程组举例
  当我们自己的程序fork出很多子进程时,所有的进程会默认的为一组,我自己程序中的原始父进程会担任组长。
  命令行终端窗口与最开始的父进程建立联系,最原始的父进程结束了,就会将终端交互权还给shell程序

8.2.4 相关API
  我们可以调用相关API将一个非组长进程,设置为一个新的进程组组长,或者说加入其它进程组。
  由于这些API在实际开发中基本用不到,所以不用了解
8.3 会话期关系
  多个进程组在一起,就组成了会话期

9. 守护进程
  守护进程也被称为精灵进程
  对于一些稍大型的软件来说,往往都是多进程的,而且其中好多进程都是默默无闻运行的,像这种默默无闻运行的进程都需要做
成守护进程
  不过由于在我们实际开发中,特别是在嵌入式开发中,根本用不到守护进程,因此我们这里就不做详细的讲解,就算讲,我们讲的
  也只是基于C实现的守护进程