3.1 linux进程 2015/7/22

来源:互联网 发布:linux如何给文件夹改名 编辑:程序博客网 时间:2024/05/20 06:05

理论知识补充

为了方便讨论,我们限定CPU为单核,单处理器,也就是说同一时刻只能执行一条指令或者一条代码的CPU。
但是我们仍可以同时运行多个程序,这是因为CPU是分时复用CPU。
通常CPU运行一条指令的时间是1ns,有的甚至更快。
1s = 1000ms
1ms = 1000μs
1μs = 1000ns

假设CPU是1GHz,Hz是一秒执行了多少次,也就是说1GHz的CPU执行一条指令需要1/1G秒=1ns,这就是时钟周期,绝大多数的机器指令都能在一个机器周期内能够完成。
机器指令周期的长短取决于指令的难易度,例如3+5的机器指令周期就是1ns,但是4/2这个机器指令就会超过1ns,大概在15ns,也就是15个时钟周期。

时间片:CPU分时复用的时间单元,是CPU分配给进程执行指令的最小时间单位,这是操作系统决定的。有的操作系统的时间片是10ms,那我们就以10ms为例,10ms=1千万ns,这是个庞大的数字,足够程序运行了

单核的CPU是程序切换的速度太快,我们感知不到,所以认为是同时运行的
多核CUP才是可以真正同时运行多个进程的

程序:没有运行,只是磁盘中的数据
进程:正在运行的一个程序


每个进程在内核中都有一个进程控制块(PCB)来维护进程相关信息,linux内核的进程控制块是task_struct结构体,下面全面了解其中有哪些信息。
  • 进程id.系统中每个进程有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数(unsigned int
  • 进程的状态:运行、挂起、停止、僵尸等状态
  • 进程切换时需要保存和恢复的一些寄存器
  • 描述虚拟地址空间的信息
  • 描述控制终端的信息
  • 当前工作目录(Current Working Directory)(有个函数chdir,修改当前目录,就是这个值)
  • umask掩码
  • 文件描述符表,包含很多指向file结构体的指针
  • 和信号相关的信息
  • 用户id和组id
  • 控制终端、Session和进程组
  • 进程可以使用的资源上限(Resource Limit)

进程的状态:运行、挂起、停止、僵尸等状态

  • 单核CPU内,同一时间只能执行一个进程,我们就说这个进程处于运行态。
  • 没有处于CPU分配的时间片内的进程,完事具备,只等CPU调用,我们就说这个进程处于就绪态。
  • 如果一个进程在处于运行态的时候,调用了Sleep这类休眠函数,那么这个进程就会让出CPU,这是这个进程就会处于挂起态。
    处于运行态的进程,当CPU被剥夺走之后,就会进入就绪态,等待时机再次进入运行态
  • 当一个进程运行结束之后,进入终止态,再就是调用return或exit函数,进程退出了,就进入终止态。
    处于挂起态的进程会等待一个条件把程序唤醒,但是唤醒之后不会理个进入运行态,而是进入就绪态,等待时机在进入运行态。
每个进程都有一个进程控制块(PCB),这PCB里面有专门记录进程状态的字段STATUS。

进程切换时需要保存和恢复的一些寄存器

当一个进程从运行态切换到就绪态或挂起态时,这个时候就要保存这个进程在运行时候各个寄存器值,当这个程序的恢复到运行态的时候,就要把保存寄存器的值恢复到CPU的寄存器当中,以保证下一运行这个进程能够正常的运行。

插入处理器现场的图

CPU大体是由    运算器(ALU算数逻辑累加单元),加法,减法,左移,右移。    寄存器堆(eax,ebx,esp,eflahs,pc等的组合,X86里面一般有17个,ARM里面一般有37个),    预取器(主要是从内存中把二进制机器指令取到CPU内部,内存的地址是从pc这个程序计数器中取得)    译码器(分析预取器中取得的指令是干什么的)

例:add r0,r1,r2这条指令,只把r1,r2相加,结果放到r0里面

    首先预取器得到从pc(程序计数器)取得这条指令的地址,然后把这条指令取到CPU内部,传给译码器,译码器分析出来该用那个寄存器作为ALU的输入单元,然后译码器去通知寄存器堆,把r1的值和r2的值输出给运算器(ALU算数逻辑累加单元),运算器完成把结果回写给寄存器堆,然后r0这个寄存器去接收结果。上述这个过程形成了流水线概念第一条流水线:预取第二条流水线:译码第三条流水线:执行    也就是说同一时刻,CPU内可能有三条不同的指令,处于不同的流水线上,但是只能有一条执行的,另外两条在做准备工作。    进程切换时需要保存和恢复的是寄存器堆里面的值,这个操作是操作系统执行的,这个寄存器的值是保存到内核的栈里面的,用户无法访问。    每个进程的进程控制块(PCB)有一个内核栈指针,指向内核的存储空间,每个进程带内核中都有一个存储空间,这个空间可以用来保存处理器现场。

虚拟地址空间的信息

插入虚拟地址转换的图

两个可执行程序a.out,b.out,1G的物理内存CPU有个MMU(内存管理单元):负责把物理地址转化成虚拟地址,设置内存访问级别    a.out,b.out运行的时候操作系统回生成4个G的虚拟地址,但这时候只是个框架,当加载代码段和数据段的时候,MMU负责把一段物理内存分配过来(物理内存是按照页面(page)来管理的,一般来说1page = 4096B,物理内存大小按照page来分配),那数据段和代码段加载的虚拟地址就相当于有真正的物理内存进行关联了,物理地址和虚拟地址之间的转换规则有MMU来决定。    多个进程虚拟内存3G到4G的kernel空间都是从物理的kernel通过MMU映射过来的       当a.out使用malloc函数申请堆空间的时候,也是通过MMU把物理内存中要申请的对空间进行关联    尽管多个进程代码段和数据段的虚拟地址都差不多,但是关联的地址空间不会重叠,也就是说每个进程都维护了一张虚拟地址和物理地址对应关系的地址转换表,这张表存在PCB里面。    以X86的CPU为例,是有四个工作级别的,分别是round0,round1,round2,round3,round0的级别最高类似linux的root权限,linux只是用了round0和round3这两个级别。    体现在虚拟地址空间上就是kernel空间和user空间,这是MMU在分配空间页(page)面的时候,把对应kernel页面的使用级别设置成round0级别,把对应user页面的使用级别设置成round3级别    CPU工作在0级的时候可以访问0级,也可以访问3级,但CPU工作在3级的时候不能访问0级(权限不够,段错误),

进程环境

libc中定义的全局变量environ指向环境变量表,没有包含在任何头文件里面,所以使用时要用extern声明,extren表中的内容可以通过shell下`env`命令查看```c#include <stdio.h>int main(void){    extern char **environ;    int i;    for(i=0; environ[i]!=NULL;i++)        printf("%s\n", environ[i]);    return 0;}```environ是个char类型的二级指针,也可以理解成一个char类型的二维数组,其中每个一维数组指向env的一行,所以通过上述代码可以把environ输出。环境变量字符串一般都是name = value这样的形式,大多数name由大写字母加下划线组成可以通过`echo $name`的形式显示环境变量的value值
#include <stdlib.h>char *getenv(const char *name);getenv的返回值是指向value的指针,若未找到则为NUL。

修改环境变量的函数

#include <stdlib.h>int setenv(const char *name, const char *value, int rewrite);void unsetenv(const char *name);putenv和setenv函数若成功则返回为0,若出错则返回非0。setenv将环境变量name的值设置为value。如果已存在环境变量name,那么若rewrite非0,则覆盖原来的定义;若rewrite为0,则不覆盖原来的定义,也不返回错误。unsetenv删除name的定义。即使name没有定义也不返回错误。

例子

#include <stdio.h>#include <stdlib.h>int main(void){    printf("PATH=%s\n", getenv("PATH"));    setenv("PATH", "hello", 1);    printf("PATH=%s\n", getenv("PATH"));    return 0;}

运行结果
$ ./a.out
PATH=/home/aaaa/Qt5.4.0//bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/aaaa/Qt5.4.0//5.4/gcc/bin
PATH=HELLO

这里看出PATH这个环境变量被修改了,但是这时候的shell命令一样可以使用,在此运行echo $PATH查看当前的环境变量
$ echo $PATH
/home/itcast/Qt5.4.0//bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/itcast/Qt5.4.0//5.4/gcc/bin

可以看出PATH的值没有被改变,这是因为每个进程都有自己的环境变量,我们修改的只是a.out这个进程的环境变量,而没有改变shell的环境变量,所以仍然可以看到shell的PATH的值,

进程状态

修改进程资源限制,软限制可改,最大值不能超过硬限制,硬限制只有root用户可以修改`cat /proc/self/limits`查看进程资源限制
$ cat /proc/self/limits Limit                     Soft Limit           Hard Limit           Units     Max cpu time              unlimited            unlimited            seconds   Max file size             unlimited            unlimited            bytes     Max data size             unlimited            unlimited            bytes     Max stack size            8388608              unlimited            bytes     Max core file size        0                    unlimited            bytes     Max resident set          unlimited            unlimited            bytes     Max processes             7892                 7892                 processes Max open files            1024                 4096                 files     Max locked memory         65536                65536                bytes     Max address space         unlimited            unlimited            bytes     Max file locks            unlimited            unlimited            locks     Max pending signals       7892                 7892                 signals   Max msgqueue size         819200               819200               bytes     Max nice priority         0                    0                    Max realtime priority     0                    0                    Max realtime timeout      unlimited            unlimited            us 

ulimit -a查看当前软限制,和修改的方式,例如修改open file的命令就是ulimit -n 2000,但是最大值不能超过硬限制。cat /proc/sys/fs/file-max查看系统可以支持创建的最大文件个数

$ ulimit -acore file size          (blocks, -c) 0data seg size           (kbytes, -d) unlimitedscheduling priority             (-e) 0file size               (blocks, -f) unlimitedpending signals                 (-i) 7892max locked memory       (kbytes, -l) 64max memory size         (kbytes, -m) unlimitedopen files                      (-n) 1024pipe size            (512 bytes, -p) 8POSIX message queues     (bytes, -q) 819200real-time priority              (-r) 0stack size              (kbytes, -s) 8192cpu time               (seconds, -t) unlimitedmax user processes              (-u) 7892virtual memory          (kbytes, -v) unlimitedfile locks                      (-x) unlimited

进程原语

进程原语:如何创建一个进程,消除一个进程等

fork

进程调用fork,生成一个当前进程的子进程。

    SYNOPSIS       #include <unistd.h>       pid_t fork(void);    RETURN VALUE       On success, the PID of the child process is returned in the parent, and 0 is       returned in the child.  On failure, -1 is returned in the parent,  no  child       process is created, and errno is set appropriately.
父进程中返回子进程ID子进程中返回0读时共享,写时复制

fork讲解图

进程A调用fork之后,会生成一个子进程,暂且叫他A子,原来的进程成为父进程当进程A调用fork之后,操作系统会分配一个0~4G的虚拟地址空间分配给A子,0~3G的代码段,数据段,堆,栈等数据完全从父进程中拷贝,3G~4G的kernel空间也会从父进程中拷贝,但是系统为了区分进程身份,会给A子进程PCB分配一个新的进程ID。当CPU运行进程A子的代码段时,代码段是完全从进程A中复制过来的,包括进程A中代码的运行状态,例如在fork之前进程A运行到90行,那fork之后的子进程里面的代码段状态也是运行到90行。fork()的返回值是pid_t,特点,一次调用,返回两次,就是说调用一次fork进程,会返回两个pid_t的值,父进程的pid_t是子进程的ID号,子进程的pid_t等于零

代码:

#include <stdlib.h>#include <stdio.h>#include <unistd.h>int main(void){    pid_t pid;    pid = fork();    if(pid == 0) {  //子进程的pid返回值是0        while(1) {            printf("I am child\n");            sleep(1);        }       }       else if  (pid > 0) {    //父进程的pid返回值是子进程的进程ID;肯定是大于0的        while(1) {            printf("I am parent\n");            sleep(3);        }       }       else {  //如果fork出错,输出出错码。例如没有创建进程所需要的内存        perror("fork");        exit(1);    }       return 0;}

fork运行图

这时候查看系统进程是可以发现两个a.out的,一个是父进程,一个是子进程,

aaaa    5710  0.0  0.0   1992   280 pts/4    T    14:30   0:00 ./a.outaaaa    5711  0.0  0.0   1992    56 pts/4    T    14:30   0:00 ./a.out

虚拟地址映射

当A子进程创建出来的时候是通过映射,和A进程共用一块物理内存物理地址分配原则:读时共享,写时复制。假设A进程中有变量i;在fock之后,A子里面也会有个i,当程序只是对i进行读操作的时候,A和A子是访问的虚拟地址是不同的,但是映射到物理内存中,物理地址是一样的,但是如果对i进行修改,也就是写操作,那这时候系统把i的内容复制一份,分别映射给A和A子,也就是说如果A把i的值改变了,那A子是不能读A里面i的内容的。根据这个原则,进程的代码段是映射的,不是复制的。

getpid/getppid

#include <sys/types.h>#include <unistd.h>pid_t getpid(void); //返回调用进程的PIDpid_t getppid(void);    //返回调用进程父进程的PID

例:

#include <stdlib.h>#include <stdio.h>#include <unistd.h>#include <sys/types.h>int main(void){    pid_t pid;    pid = fork();    if(pid == 0) {  //子进程的pid返回值是0        while(1) {            printf("I am child %d, my parent is %d\n", getpid(), getppid());            sleep(1);        }       }       else if  (pid > 0) {    //父进程的pid返回值是子进程的进程ID;肯定是大于0的        while(1) {            printf("I am parent %d, my parent is %d\n", getpid(), getppid());            sleep(3);        }       }       else {  //如果fork出错,输出出错码。例如没有创建进程所需要的内存        perror("fork");        exit(1);    }       return 0;}

结果

$ ./a.out I am parent 6123, my parent is 4889I am child 6124, my parent is 6123I am child 6124, my parent is 6123I am child 6124, my parent is 6123I am parent 6123, my parent is 4889......

ps aux查看父进程的父进程什么

aaaa    4889  0.0  0.6  11436  6392 pts/4    Ss+  13:02   0:01 -bash
-bash进程,也就是shell进程,a.out的父进程是shell进程

#**结构图**

exec族

从磁盘当中去加载一个程序,用这个程序的代码段,数据段,堆,栈等来替换当前空间里面的代码段,数据段,堆,栈等(加载新的执行程序,替换当前进程空间里的代码段数据段,堆和栈).
#include <unistd.h>int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);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[]);int execve(const char *path, char *const argv[], char *const envp[]);

int execl(const char *path, const char *arg, …);

execl列表形式:因为要加载的程序可能需要向里面传参数,可以用这个函数的其他形参往里面传,以最后一个参数NULL作为结尾例:main.c使用execl函数调用test程序main.c 生成a.out
#include <stdio.h>#include <unistd.h>int main(void){    printf("hello world\n");    execl("test", "1111111", "asdf", NULL);    printf("successful\n");    return 0;}
test.c 生成test
#include <stdio.h>int main(int argc, char *argv[]){    int i = 0;    for(i = 0; i < argc; i++)        printf("%s\n", argv[i]);    return 0;}
执行结果
$ ./a.out hello world1111111asdf
  • 类比理解shell:在shell下面执行a.out的时候,shell也是先fork创建一个子进程,然后exec把a.out加载进来,运行完之后,shell的父进程回到前台

int execlp(const char *file, const char *arg, …);
execlp是把execl的基础上把环境变量加上了,可以直接使用shell命令了,不用再加上路径就可以使用了
例:

    execlp("ls", "1s", "-l", NULL);

int execv(const char *path, char *const argv[]);
就是把execl后面的参数放到了一个指针数组里面,仅此而已。

execl("test", "1111111", "asdf", NULL);

等同于

char *argvv[] = {"1111111", "asdf", NULL}execv("test", argvv);

int execvp(const char *file, char *const argv[]);

execvp是把execv的基础上把环境变量加上了,可以直接使用shell命令了,不用再加上路径就可以使用了

int execle(const char *path, const char *arg, …, char *const envp[]);

int execve(const char *path, char *const argv[], char *const envp[]);
可以指定被调用程序的环境变量,这个环境变量可以是一个字符数组,也可以是一个指向字符数组的指针

#include <stdlib.h>#include <unistd.h>int main(void){    char *argv[] = {"ls", "-l", ".", NULL};    //char *envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};    char envp[] = {"PATH=/bin:/usr/bin"};    execve("./test", argv, envp);    return 0;}
事实上,只有execve才是真正的系统调用,其他五个函数最终都是指向execve,所以execve在man手册第2节,其他函数在man手册第3节。l 命令行参数列表p 搜素file时使用path变量v 使用命令行参数数组e 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量

wait/waitpid

僵尸进程:子进程退出,父进程没有回收子进程资源(PCB),则子进程变成僵尸进程孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为1号进程init进程,成为init进程领养孤儿过程

wait

回收子进程,会收成功,返回子进程ID,失败返回-1,这是一个阻塞函数

#include <sys/types.h>#include <sys/wait.h>pid_t wait(int *status);

例子

int main(void){    pid_t pid;    pid = fork();    if(pid == 0) {        printf("I am child\n");        sleep(3);    }       else if(pid > 0) {        printf("begin wait\n");        wait(NULL);        printf("after wait\n");        while(1) {            printf("I am parent\n");            sleep(1);        }       }       else {        perror("fork");        exit(0);    }   }

运行结果

$ ./wait begin waitI am childatfer waitI am parentI am parent........
通过执行结果,父进程等子进程死掉回收才会继续执行,可以看出wait是一个阻塞函数,,等待子进程结束,再往下执行`wait(NULL);`

pid_t wait(int *status);里面的int *status是一个传出变量
子进程中的status是一个32位的整型,子进程结束之后,PCB里面的部分数据会存到这个整型里面,主要范围两个区域,信号值和退出值
当子进程正常return 0退出的时候,status返回的时候会退到最低8位退出值,并设为0
当子进程是被kill掉的,那信号值里边就会记录是被几号信号kill的。

    孤儿进程:当父进程先于子进程死掉,那子进程的父进程ID会变成1,也就是init进程领养,这是,子进程还会继续执行,并且ctrl+c是终止不了的,这是因为父进程死掉之后,shell回到前台,这个时候ctrl+c是被shell接收到的,而子进程是接收不到,只能kill掉

waitpid

可以指定回收进程的进程ID或进程组ID,int options可以设置非阻塞还是阻塞,执行成功返回子进程的进程ID,失败返回-1;在等待子进程结束时,有返回0;

#include <sys/types.h>#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);
pid_t pid的取值含义< -1 回收指定进程组内的任意子进程-1 回收任意子进程0 回收和当前调用waitpid一个组的所有子进程> 0 回收指定ID的子进程

ps -ajx 可以查看进程的组ID
kill 9 -6994说明杀死6994这个进程组,不加-号只杀进程id

< -1 回收指定进程组内的任意子进程,小于-1的只可能是进程组,是回收这个进程组的子进程

wait和waitpid的区别:    如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid是如果在options参数中指定WNOHANG可以是父进程不阻塞而立即返回0.    wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/types.h>#include <sys/wait.h>void sys_err(const char *str){    perror(str);}int main(void){    pid_t pid;    pid_t wpid;    if((pid = fork()) < 0) {        sys_err("fork");    }       else if(pid == 0) {        int i = 5;          while(i--) {    //子进程运行5次            printf("i am child\n");            sleep(1);        }       }       else {        while(1) {            wpid = waitpid(0, NULL, WNOHANG);   //指定回收跟父进程一个进程组的子进程,不返回status,不阻塞父进程。            printf("i am parent, wpid = %d\n", wpid);            sleep(1);        }       }       return 0;} 

status参数的用法

int main(void){    pid_t pid;    if((pid = fork()) < 0) {        perror("fork");        exit(-1);    }       else if(pid == 0) {        int i;        for(i = 3; i > 0; i--) {    //打印三次            printf("This is the child\n");            sleep(1);        }           exit(3);    //exit退出子进程    }       else {        int stat_val;        waitpid(pid, &stat_val, 0);     //option为零的时候,waitpid和wait一样,具有阻塞属性        if(WIFEXITED(stat_val)) //WIFEXITED宏函数,检测stat_val字段是不是正常退出的,正常退出包含exit,_exit,return             printf("CHild exited with code %d\n", WEXITSTATUS(stat_val));   //WEXITSTATUS宏函数,如果子进程是正常会出的,退出值是多少。        else if(WIFSIGNALED(stat_val))  //如果不是正常退出的,WIFSIGNALED检测子进程是否是被信号杀死的            printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val))  //如果是被信号杀死的,WTERMSIG是被几号信号杀死的    //可以通过kill -l查看几号信号对应什么指令。    };    return 0;}
0 0