Linux基础(13)进程基础

进程的基本概念: Linux中事务管理的基本单元 ,代表资源的总和 再比如 ,进程是一座大厦里面的水电空间都是资源 , 而线程就是一家家的公司占用着大厦的资源

  1.创建进程fork(): 在当前进程复制出一个子进程 ,子进程和父进程相同互不影响 ,若成功调用一次则返回两个值,子进程中fork返回0,父进程中fork返回子进程ID;否则,出错返回-1

进程类型:  https://www.cnblogs.com/renyz/p/11218776.html

  僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。用自己的话表达:父进程退出了,子进程没有退出,那么这些子进程就没有父进程来管理了,就变成僵尸进程。

   交互进程:是由一个shell启动的进程。交互进程既可以在前台运行,也可以在后台运行;

  批量处理进程:这种进程和终端没联系,是个进程序列;

  守护进程:在Linux系统启动时启动的进程,并在后台运行;

  超级守护进程:系统启动时由一个统一的守护进程xinet来负责管理一些进程,当相应请求到来时需要通过xinet的转接才可以唤醒被xinet管理的进程。

  2. 进程状态: https://www.cnblogs.com/diegodu/p/9167671.html

#define TASK_RUNNING          0      //可执行状态
#define TASK_INTERRUPTIBLE     1      //可中断的睡眠状态
#define TASK_UNINTERRUPTIBLE    2      //不可中断的睡眠状态
#define __TASK_STOPPED         4      //暂停状态
#define __TASK_TRACED          8      //跟踪状态
#define EXIT_ZOMBIE           16      //将死状态
#define EXIT_DEAD         32      //退出状态

    进程只有处在TASK_RUNNING 状态才能被运行 ,因为系统每10毫秒就会执行一次 schedule(遍历task[]) ,

    只有处在TASK_RUNNING 状态的进程才会被发送到CPU的可执行队列里

  3. 进程在内核实现里是面向struct task_struct对象 ,task_struct是早期出现的,在kernel 3.0+的版本变成了struct cred ,也就是进程控制模块PCB ,

    在早期kernel 0.1 fork()的实现是调用两个函数 find_empty_process(查找全局的task[MAX = 64]进程链表并返回有NULL的下标) , 

    copy_process()找到空位后对该对象 task[ i ] 进行设置(设置task[i]->) task_struct对象,设置好后利用copy_mem()把要fork的进程的栈拷贝到task[i]里

  4. getpid()获得当前进程号 , getppid()获得当前进程的父进程号 ,getpgid()获得进程组ID ,由父进程创建的子进程都是一个组里的

  5. getsid()获得会话ID setsid()设置会话ID  比如通过SSH连接Linux后 ,终端(作为一个进程)上开启的进程都是同一个会话ID且sessionID和组ID有可能是一样 ,在同一个父进程下创建的子进程他们的uid也是相同的(孤儿进程的UID就不相同 ,但sessionID还是相同的)

  6. 守护进程: 在一个A进程下fork一个B进程 ,而AB进程的sessionID是一样的 ,在A进程被结束掉后避免B进程变成孤儿进程就为B进程setsid一个新的sessionID(pgid ,根目录和工作目录都必须改变与A进程不相同) ,这样就摆脱了A进程变成一个后台进程 ,而B进程也作为一个新的父进程也是后台进程继续完成后续的操作

  7. 控制终端: 一个会话可以有一个终端 ,控制进程就是打开终端的进程(只有控制进程才能结束当前终端) . 终端发送的信号(kill -9)只有前台进程组的进程收到,后台进程不处理 ,再比如: SSH断开连接后终端将发送断开的信号发送给前台进程组的组长达到控制的目的

  8.tcgetpgrp()获得前台进程组的ID ,tcgetsid()获取终端的会话组的首进程sessionID

  9. uid 进程用户的ID ,一般表示进程的创建者(属于哪个用户创建)  ,uid:0代表根ID , 1~1000为系统保留的ID, 1000往下才是用户自定义的ID

   euid进程的有效用户ID表示进程对于文件和资源的访问权限(具备等同于哪个用户的权限) , 可通过setuid临时将该进程设置为文件(进程)拥有者权限  ,

   比如ping的拥有者是root ,本来普通用户是无法执行的,但是该进程设置了setuid(普通用户临时被赋予了文件拥有者的权限) ,就算是普通用户也可以执行 ,

   https://blog.csdn.net/weixin_44575881/article/details/86552016  https://blog.csdn.net/suqin0802/article/details/7336779

  10. gid 进程的组ID ,egid  文件(进程)拥有者所在的组,进程的有效组ID


 

  进程的管理及控制

  1.进程的创建

    fork(),创建进程时注意fork对IOFILE中的缓存进行拷贝等坑点(比如缓冲区还存在数据时就被fork ,导致子进程的该缓冲区也存在该数据),最好先用fflush()把缓冲区里的数据排出到0里

pid_t pid;
printf("1/n");
printf("2\t");
fflush(0);    //因为printf是行缓冲 ,所以2还在0的缓冲区内,如果不fflush(0),那么父子两个进程的缓冲区都会存在2
pid = fork();
if( pid == 0 )
{
    printf("fork: child pid = %d" ,getpid());
}else
{
    printf("fork: father pid = %d",getpid());
}

task_struct //进程

     问:在父进程open了一个文件后fork , 那么子进程是否也可以使用该文件?  答:可以 ,因为fork复制的是整个struct_IO_FILE_plus结构体 , 而这个结构体里有链表(存放文件描述符) 和虚函数表(面向当前对象fd的操作方法等等),而不是单独复制描述符或描述符指向的文件 ,父子进程因为struct_IO_FILE_plus结构体都是相同的,所以其指向的文件是两个进程共享的 (就好比父子进程的printf都能打印在当前控制台上 ,当然,他们的打印先后是CPU决定的)

     socketpair(fd[])其效果也是和pipe()一样,建立一对匿名的已经连接的套接字,可通过fd句柄进行亲缘关系进程间的通信,是双向的

Linux基础(13)进程基础
#include <sys/types.h>
#include <sys/socket.h>
    
#include <stdlib.h>
#include <stdio.h>
    
int main ()
{
    int fd[2];
   
    int r = socketpair( AF_UNIX, SOCK_STREAM, 0, fd );
    if ( r < 0 ) {
        perror( "socketpair()" );
        exit( 1 );
    }
   
    if ( fork() ) {
        /* Parent process: echo client */
        int val = 0;
        close( fd[1] );
        while ( 1 ) {
            sleep( 1 );
            ++val;
            printf( "Sending data: %d/n", val );
            write( fd[0], &val, sizeof(val) );
            read( fd[0], &val, sizeof(val) );
            printf( "Data received: %d/n", val );
        }
    }
    else {
        /* Child process: echo server */
        int val;
        close( fd[0] );
        while ( 1 ) {
            read( fd[1], &val, sizeof(val) );
            ++val;
            write( fd[1], &val, sizeof(val) );
        }
    }
}                
socketpair

     vfork: 在Linux是没有线程概念的,线程是由第三方实现的 , vfork是轻量级的进程也是线程的前身 , vfork不是所有东西都复制的 ,代码段,数据段 ,全局变量及主线程vfork前的局部变量等都是和父进程共享的 , 只有必要时才新申请内存(比如在其中一个进程定义一个局部变量或申请一个内存时 ,才新申请内存)

  2.进程的代码执行

  3. 回收进程资源(进程的退出)

  4.在当前进程启动另外一个进程时有两种方式 1.system("/bin/sh") 。2.exec用被执行的程序完全替换调用它的程序的影像。fork创建一个新的进程就产生了一个新的PID,exec启动一个新程序,把原来的程序除执行单元(代码段)外 ,其余的资源(数据段 堆 栈等)都和新程序共享,因此让有新PID的新程序可以取代旧程序进行执行其他操作

   exec函数族: https://blog.csdn.net/zhengqijun_/article/details/52852074   

         execl(const char *path, const char *arg, ...) 传的参是文件路径 第二个开始的参数数量是可变的,arg0 ,arg1 ... 一般都使用execl

         execv(const char *path, char * const argv[]);传的参是文件路径 ,第二个参数是一个指针数组不用同上一样一个个的手动传参

         execlp(const char *file, const char *arg, ...)传的参是文件名(系统会从$PATH里找到目录) l使用路径  p使用文件名根据环境变量PATH 

         execve(const char *path, char * const argv[], char* const envp[](指定环境变量))是真正意义上的系统调用,其他的都是在此基础上封装的库函数

                                             https://blog.csdn.net/David_361/article/details/86057551

         只记execl()和execve()就可以了 execl的可变参数末尾必须是0结尾 ,execve是指针数组每个指针都是一个参数, exec的程序可以只给第一个参数但是argv[0]是必须的 ,传入文件名

#include<stdio.h>
#include
<unistd.h> #include<sys/types.h> #include<fcntl.h> #include<string.h> #include<stdlib.h> int main(int argc,char *argv[]) { int fd,status; pid_t pid; fd=open("test.txt",O_RDWR|O_APPEND|O_CREAT,0644); if(fd==-1) { perror("open"); exit(EXIT_FAILURE); } //fcntl(fd,F_SETFD,FD_CLOEXEC); //include this ,will error printf("befor child process write\n"); system("cat test.txt"); if((pid=fork())==-1) { perror("fork"); exit(EXIT_FAILURE); } if(pid==0) { char buf[128];
     char* argv1[] = {
      "newcode",
      fd
,
      "0"
     }
sprintf(buf,"%d",fd);
//execl("./newcode","newcode",buf,(char *)0);
     execve("./newcode" ,argv1,NULL);
}
else
{
wait(&status);
printf("after child_process write\n");
system("cat test.txt");
}
}

newcode.c

int main(int argc,char *argv[])
{
int i;
int fd;
char *ptr="helloworld\n";
fd=atoi(argv[1]);  //当前进程是无法通过argv[1]找到test.txt的, 但是在exec的情况下被调用进程是和调用进程共享资源的,因此可以通过argv[1]=fd找test
i=write(fd,ptr,strlen(ptr));
if(i<=0)
perror("write");
close(fd);
}

  5.回收进程(退出) 

    退出程序有两种方式在main函数里return或者exit, exit()比return多做了一些事,比如通过no_exit注册一些函数在调用exit时会回调注册的函数

    exit在内核的过程: exit()   ----->  __GI_exit()   ------>  call   __run_exit_handlers() ------>  test_exit()

    exit()之后会调用 fflush函数把当前进程没打印完的字符串都打印出去

    孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

    僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

    孤儿和僵尸进程归init进程管理  https://www.cnblogs.com/guxuanqing/p/10528260.html

void test_exit(int status, void*arg)
{
  printf("befor exit\n");
  printf("exit %d\n",status);    //status是退出码 100
  printf("arg=%s\n",(char*)arg);  //         test
}

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    exit(1);
  }
  else if(id == 0)
  {
    char* str = "test";
    on_exit(test_exit.(void*)str);
    exit(100);
  }
  return 0;
}

    wait() waitpid() : 回收退出进程的资源 https://www.cnblogs.com/king-77024128/articles/2684317.html

    wait(&status) 可以传入一个int 参数(返回子进程退出状态) 可以用 WIFEXITED(status)解析子进程的退出是否正常 ,如子进程exit(5) ,WIFEXITED()返回5

    waitpid(pid_t pid,int* status,int options)第一个参数有4种, 等待成功会返回结束的进程PID ,没成功会返回0

                          1.pid>0等待指定进程结束 ,2. pid=-1等待任意子进程结束(相当于wait) ,

                          3.pid=0等待与调用进程的pgid(同一父进程或父进程)一致的进程结束

                          4.pid<-1 等待指定进程组内的任意进程 ,比如-5那么就是 进程组5内的任意进程

           第二参数作为返回值 可以使用 WIFEXITED(status) , 第三参数代表状态 ,0函数阻塞 ,1函数非阻塞

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

int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0){
        printf(" fork error: %d \n",errno);
        return 1;
    }
    else if(pid == 0){
        //child
        printf("I am child..pid is :%d\n",getpid());
        sleep(5);
        exit(257);
    }
    else{
        int status = 0;
        pid_t ret =waitpid(-1,&status,0);//blocking waiting for 5S
        printf("this is test for wait\n");
        if(WIFEXITED(status) && ret == pid){
           printf("wait child 5S success,child return code is:%d.\n",WEXITSTATUS(status));
        }else{
            printf("wait child failed.return \n");
            return 1;
        }
    }
    return 0;
}

                      非阻塞

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


int main()
{
  pid_t pid;
  pid = fork();
  if(pid < 0){
    printf("fork error: %d \n",errno);
    return 1;
  }
  else if(pid == 0){
    printf("child is run .pid id : %d\n",getpid());
    sleep(5);
    exit(1);
  }
  else{
    int status = 0;
    pid_t ret = 0;
    do{
      ret = waitpid(-1,&status,WNOHANG);//非阻塞等待
      if(ret == 0){
        printf("child is running\n");
      }
    sleep(1);
    }while(ret == 0);
    if(WIFEXITED(status) && ret == pid){
      printf("wait child 5s success,child return code is :%d\n",WEXITSTATUS(status));
    }else{
      printf("wait child failed,return.\n");
      return 1;
    }
  }
return 0;
}

    6.守护进程(后台进程)    https://blog.csdn.net/xu1105775448/article/details/80877747

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>

void creat_daemon()
{
    int i,fd;
    pid_t id;
    struct sigaction sa;
    umask(0);//第一步:调用umask将文件模式创建屏蔽字设置为0 ,open(fd,flags,mode)把文件掩码mode设置成0
    if((id = fork()) < 0){
        printf("fork error!\n");
        return;
    }else if(id != 0){
        exit(0);//第二步:调用fork,父进程退出。保证子进程不是话首进程,从而保证后续不和其他终端关联。
    }

    setsid();//第三步:设置新会话。 让子进程脱离和父进程一样的会话id 作为一个新会话的首进程

    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if(sigaction(SIGCHLD,&sa,NULL) < 0){
        //注册子进程退出忽略信号
        return;
    }
  
    if((id = fork()) < 0){  //如果直接使用第一个子进程,虽然sid和父进程不同了,但是和父进程在同一进程组,要完全脱离父进程使用子进程再创建一个子进程可以了
        printf("fork 2 error!\n");
        return;
    }else if(id != 0){
         exit(0);
    }
    //    守护进程空间
    if(chdir("/") < 0){
        //第四步:更改工作目录到根目录 为了和父进程完全脱离关系,避免父进程退出后子进程找不到工作路径等情况 从而需要重新设置子进程的工作目录和根目录
        printf("child dir error\n");
        return;
    }
    close(0);  //关闭从父进程继承到的stdin(stdin可能会重定向到某个文件中) ,避免这种情况出现需要重新打开stdin重定向所以的标准到/dev/NULL
                    //关闭了stdin ,再重新打开一个空的文件,那么系统会重新分配stdin fd
= open("/dev/null",O_RDWR);//关闭标准输入,重定向所有标准(输入输出错误)到/dev/NULL dup2(fd,1); dup2(fd,2); } int main() { creat_daemon(); while(1){ sleep(1); } }

     7.日志进程


 

总结: 在Linux内核里管理多进程的是一个stak_struct链表 ,进程是通过uid和guid识别的如果这两个都是0那么代表root ,

    fork()在内核的实现 ,用find_empty_process()查找全局的stak[]链表的空位 ,再利用copy_process()根据父进程给新的子进程(stak_struct结构)设置对象,代码段 ,数据段等也随之复制进子进程里 而这个子进程也放进stak[]里。 在复制文件IO时 ,是复制整个struct file整个结构体而不是单独复制一个文件描述表 

stak_struct结构

Linux基础(13)进程基础

上一篇:js中的execCommand()方法妙用


下一篇:SRv6 - Linux Kernel Implementation