Linux进程

来源:互联网 发布:中国造不出圆珠笔知乎 编辑:程序博客网 时间:2024/06/05 03:39

1. execve()函数

前面讲到的execX函数簇,它们都是标准库函数里的函数,也就是说它们是对系统调用API的封装,这个系统调用的API就是execve()函数,原型为:

int execve(const char *filename, char *const argv[], char *const envp[]);

调用方法还是类似之前:

// 主调进程int main(void){    char* argv[] = {        "hello", "world", NULL    };    char* envp[] = {        "PATH = abcdef", "a = wwww", NULL    };    execve("./new_pro", argv, envp);    return 0;}
// 新启动的程序extern char** environ;int main(int argc, char** argv){    printf("new_pro: pid = %d, ppid = %d\n", getpid(), getppid());    // 打印主调进程传来的参数    while (*argv != NULL)        printf("%s ", *(argv++));    printf("\n");    // 打印主调进程传来的环境变量    while (*environ != NULL)        printf("%s ", *(environ++));    printf("\n");    return 0;}

运行结果:
这里写图片描述

2. fork()函数

为了让新程序不覆盖主调进程的进程空间,引入了fork()系统调用,即创建子进程。

int main(void){    pid_t pid;    pid = fork();    printf("pid = %d\n", pid);    return 0;}

运行结果:
这里写图片描述
代码”printf(“pid = %d\n”, pid);”被执行了两次,分别是子进程和父进程(即当前进程)执行的。
在32位的操作系统中,每个进程的进程片空间是4G,其中0-1G是内核空间,2-3G是用户空间。当然,这个空间指的是虚拟内存。
fork()函数里面的实现体可以划分为两部分: 创建进程和复制把父进程的进程空间赋值给子进程,创建完子进程后,主调进程的进程片4G空间被复制给子进程4G空间。执行流程可以概图如下:
这里写图片描述
在父进程中返回的是子进程的pid,所以是返回值大于0。在子进程返回0,0在这里只是作为子进程的标志。

2.1 子进程的进程空间由父进程的内存空间复制而得

子进程的进程空间是由父进程的内存空间复制而得,那么在父进程fork()之前的文件描述符、变量也会复制给子进程了。

int main(void){    pid_t pid;    char buf[10] = {};    printf("main, pid = %d\n", getpid());    int a = 10;    int fd;    fd = open("./test.file", O_CREAT | O_RDWR, 0666);    if (fd < 0)    {        perror("open");        return -1;    }    if ((pid = fork()) < 0)    {        perror("fork");        return -1;    }    if (pid == 0)    {        printf("child, pid = %d\n", getpid());        printf("child, &a = %p\n", &a);        a += 2;     //子进程中修改父进程复制来的a变量        if (read(fd, buf, sizeof(buf)) < 0)        {            perror("read");            exit(1);        }        printf("child read success!!\n");        exit(1);    }    printf("parent, &a = %p\n", &a);    printf("parent, a = %d\n", a);    close(fd);    return 0;}

运行结果:
这里写图片描述
子进程仍能访问父进程的文件描述符;在子进程中修改变量a,因为它修改的是父进程a的副本,所以并不会影响父进程的变量a。注意,在子父进程中,即使a的地址是一样的,但是这仅仅是虚拟空间的地址一样。子父进程拥有各自的4G虚拟进程空间,它们被映射到内存不同的两个块物理地址,物理地址不同,所以a变量互不干涉。

2.2 子进程独立虚拟内存空间

如上所说的,子父进程是完全不干涉的两个进程空间,所以它们之间的不能直接通讯。但是注意,两个进程的头1G是内核空间,内核空间是共同的,所以要让二者通讯,就需要借助内核的API,说白了就是系统调用。

2.3 子进程创建成功后,父子进程同时运行

从上面几个程序的结果可知,子进程创建成功后,父子进程是同时运行的。由于父进程直接回往代码下执行,所以先退出了,接着子进程运行完退出。有些时候,父进程要等到子进程退出后才能退出,这就需要wait()或者waitpid()系统API,其原型如下:

 pid_t wait(int *status); //阻塞等待子进程退出,只要子有一个子进程退出,该函数得到返回 pid_t waitpid(pid_t pid, int *status, int options); //指定等待哪一个进程,options可以取消阻塞等待

返回值是pid, 也就是返回退出的子进程的pid;参数status用于保存进程的退出状态,若不想获取该状态,可以设为NULL。若父进程想知道子进程的退出状态,可利用如下几个函数可以解析status:

WEXITSTATUS(status) //程序运行中,有时候我们会返回自定义的错误标号,这个宏就是来捕捉exit的错误编号(0-255)WIFEXITED(status)   //若自己退出,返回为1否则返回0WIFSIGNALED(status) //若被其他进程kill掉,返回为1否则返回0...

3. execve()函数的参数

程序在操作系统中运行,就会形成一个进程。每个进程都有自己的pid。通过命令”ps -aux”可以看到当前系统中正在运行的进程以及进程状态。常见进程状态如下:

D 无法中断的休眠状态(通常IO的进程)R 正在运行中T 停止或被追踪S 处于休眠状态Z 僵尸进程< 优先级高的进程N 优先级较低的进程

看下面代码:

// 主调进程int main(void){    int pid;    char* argv[] = {        "hello", "world", NULL    };    char* envp[] = {        "PATH = abcdef", "a = wwww", NULL    };    printf("main: pid = %d, ppid = %d\n", getpid(), getppid());    pid = fork();    // 创建子进程来执行新程序    if (pid == 0)    {        // 执行新程序        execve("./new_pro", argv, envp);    }    else        getchar();     return 0;}
// 新程序extern char** environ;int main(int argc, char** argv){    printf("new_pro: pid = %d, ppid = %d\n", getpid(), getppid());    while (*argv != NULL)        printf("%s ", *(argv++));    printf("\n");    while (*environ != NULL)        printf("%s ", *(environ++));    printf("\n");    printf("new_pro: ");    getchar();    return 0;}

运行结果:
这里写图片描述
此时运行”pa -aux”得到:
这里写图片描述
可以看到a.out进程却看不到new_pro进程。这个是跟execve()函数的形参有关。argv[0]默认是进程的名称,所以要想在在进程查看命令中看到新程序进程,需要传入新程序进程名称,argv的定义为:

 char* argv[] = {        "/new_pro", "hello", "world", NULL    };

再次运行:
这里写图片描述

3. 孤儿进程

父进程创建子进程,子进程在运行中,父进程先结束,子进程就会变成孤儿进程。

int main(void){    printf("main, pid = %d\n", getpid());    if (fork() == 0)    {        // 子进程        printf("child, pid = %d, ppid = %d\n", getpid(), getppid());        sleep(10);   // 子进程在休眠的10s间,父进程已经退出        printf("\nchild, pid = %d, ppid = %d\n", getpid(), getppid());              exit(0);    }    sleep(2);    printf("main is exit!\n");    return 0;}

运行结果 :
这里写图片描述
子进程尚在运行,若父进程退出了,那么其ppid将更改为1,也就是init进程。这个进程是Linux的第一个启动的进程,所有的进程都是由它而来的。

4. 僵尸进程

子进程结束后,父进程并未结束且没有对子进程做任何操作,子进程就为僵尸进程状态存在。子进程等待父进程去取子进程的运行结果或者执行状态。

int main(void){    printf("main, pid = %d\n", getpid());    if (fork() == 0)    {        printf("\nchild, pid = %d, ppid = %d\n", getpid(), getppid());              exit(0);    }    getchar();    printf("main is exit!\n");    return 0;}

运行结果:
这里写图片描述
此时在另一窗口执行命令”pa -aux”:
这里写图片描述
pid = 10697的子进程就变为僵尸进程了。当父进程退出后,该僵尸进程才会消失。僵尸进程的解决办法是父进程调用wait()函数等待子进程:

int main(void){    printf("main, pid = %d\n", getpid());    if (fork() == 0)    {        printf("\nchild, pid = %d, ppid = %d\n", getpid(), getppid());              exit(0);    }    wait(NULL);    getchar();    printf("main is exit!\n");    return 0;}
0 0
原创粉丝点击