alin的学习之路(Linux系统编程:六)(进程、无名管道)

alin的学习之路(Linux系统编程:六)(进程、无名管道)

进程相关

1. 父子进程的不同点

子进程拥有自己唯一的进程号

子进程没有继承父进程内存锁

子进程的资源没有被初始化

子进程的CPU计数器被重置为0

子进程的阻塞信号集被初始化为空

子进程没有继承信号量

子进程没有继承父进程关联的记录锁

子进程没有继承定时器

子进程没有继承IO操作

读时共享,写时拷贝

仅当对数据进行写修改时才拷贝数据,优化了fork函数

2.父子进程的执行顺序

父子进程谁先执行谁后执行不确定。

父子进程进程名一样,但进程号不同。终端相同。

区分父子进程:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    int i=0;
    pid_t pid = -1;

    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if(0 == pid)
    {
        while(i < 10)
        {
            printf("子进程 do work %d\n",i++);
            sleep(1);
        }
    }
    else
    {
        while(i < 15)
        {
            printf("父进程 do work %d\n",i++);
            sleep(1);
        }
    }
    return 0;
}

3. 父子进程与全局变量

父子进程两个都包含全局变量,遵循读时共享,写时拷贝的原则,当一方对变量进行写操作的时候,将被操作的变量分别拷贝到父子进程中,随后进行写操作,所以父子进程一方对变量进行修改,另一方不变

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int var = 100;
int main()
{
    int num = 88;
    pid_t pid;

    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if(0 == pid)
    {
        printf("子进程中:var = %d,num = %d\n",var,num);
        ++var,++num;
        printf("子进程中:var = %d,num = %d\n",var,num);

    }
    else
    {
        sleep(1);
        printf("父进程中:var = %d,num = %d\n",var,num);
    }

    return 0;
}

4.父子进程与堆区数据

当指针p指向的堆空间创建在fork之前,那么这个堆空间在父子进程中有着相同的虚拟地址(逻辑地址),但它们对应实际的物理地址不同,所以,需要在父进程和子进程中都调用free函数来释放空间避免内存泄漏

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
//父子进程与堆空间
int main(void)
{
    int ret = -1;
    pid_t pid = -1;
    int *p = NULL;
    //分配空间
    p = malloc(sizeof(int));
    if (NULL == p)
    {
        printf("malloc failed...\n");
        return 1;
    }
    memset(p, 0, sizeof(int));
    //创建子进程
    pid = fork();
    if (-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if (0 == pid)
    {
        *p = 88;
        //子进程
        printf("子进程 p = %p *p = %d\n", p, *p);
        //free(p);
    }
    else
    {
        //保证子进程先执行
        sleep(1);
        //父进程
        printf("父进程 p = %p *p = %d\n", p, *p);
        //free(p);
    }
    return 0;
}

同时可以使用valgrind ./可执行文件查看heap内存malloc与free的情况

5.gdb多进程调试

使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程

# 设置默认调试子进程
set follow-fork-mode child

# 设置默认调试父进程
set follow-fork-mode parent

如果子进程又创建了进程,则需要在子进程创建进程的fork前设置以上操作

必须在fork之前设置才有效。

6.exit 退出进程

exit函数

#include <stdlib.h>
void exit(int status);
功能:
	进程正常退出
参数:
	status 退出状态码
返回值:

_exit函数

#include <unistd.h>
void _exit(int status);
功能:
	进程退出
参数:
	status 状态码
返回值:#include <stdlib.h>
void _Exit(int status);
功能:
	进程退出
参数:
	status 状态码
返回值:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    //printf("Hello world");
    //exit(0);

    printf("hello world");
    //_exit(0);    //该函数会立即退出,不做一些处理
    _Exit(0);

    return 0;
}

主要区别:

  1. exit 退出进程会做一些额外的操作,例如:刷新缓冲区等
  2. _exit 退出进程则是会立即退出进程,不会刷新缓冲区

注意:printf 会将要输出的内容放到缓冲区中,如果不包含 \n ,则不会刷新缓冲区将内容输出

7.wait 和 waitpid 回收子进程

wait函数

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
功能:
    等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
    status : 进程退出时的状态信息。
返回值:
    成功:已经结束子进程的进程号
    失败: -1

调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。

若调用进程没有子进程,该函数立即返回;若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源。

所以,wait()函数的主要功能为回收已经结束子进程的资源。

如果参数 status 的值不是 NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。

这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。

宏函数可分为如下三组:

  1. WIFEXITED(status)

为非0 → 进程正常结束

WEXITSTATUS(status)

如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)

  1. WIFSIGNALED(status)

为非0 → 进程异常终止

WTERMSIG(status)

如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。

  1. WIFSTOPPED(status)

为非0 → 进程处于暂停状态

WSTOPSIG(status)

如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。

WIFCONTINUED(status)

为真 → 进程暂停后已经继续运行

waitpid 函数

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
功能:
    等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。

参数:
    pid : 参数 pid 的值有以下几种类型:
      pid > 0  等待进程 ID 等于 pid 的子进程。
      pid = 0  等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
      pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
      pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。

    status : 进程退出时的状态信息。和 wait() 用法一样。

    options : options 提供了一些额外的选项来控制 waitpid()0:同 wait(),阻塞父进程,等待子进程退出。
            WNOHANG:没有任何已经结束的子进程,则立即返回。
            WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)
                 
返回值:
    waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
        1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
        2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 03) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;

示例代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    int status;

    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if(0 == pid)
    {
        for(int i=0 ;i<25 ;++i)
        {
            printf("子进程do work %d\n",i);

            sleep(1);
        }
        exit(0);
    }
    else
    {
        pid = wait(&status);
        if(-1 == pid)
        {
            perror("wait");
            return 1;
        }
        if(WIFEXITED(status))
        {
            printf("子进程:%d, 退出码:%d\n",pid,WEXITSTATUS(status));
        }
        if(WIFSIGNALED(status))
        {
            printf("子进程:%d, 被信号:%d终止\n",pid,WTERMSIG(status));
        }


    }

    return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    int status;

    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if(0 == pid)
    {
        for(int i=0 ;i<5 ;++i)
        {
            printf("子进程do work %d\n",i);

            sleep(1);
        }
        exit(0);
    }
    else
    {
        pid = waitpid(-1,&status,WNOHANG);
        if(-1 == pid)
        {
            perror("wait");
            return 1;
        }
        if(0 == pid)
        {
            printf("没有可回收的子进程,父进程不等了!\n");
            return 1;
        }
        if(WIFEXITED(status))
        {
            printf("回收了子进程:%d, 退出码:%d\n",pid,WEXITSTATUS(status));
        }
        if(WIFSIGNALED(status))
        {
            printf("回收了子进程:%d, 被信号:%d终止\n",pid,WTERMSIG(status));
        }

        printf("父进程结束\n");
    }
    return 0;
}

8. 孤儿进程

当子进程未执行完毕,父进程就执行结束,此时该子进程变为孤儿进程,且该孤儿进程会被进程号为1的 init 进程收养,由 init 来管理它的回收与资源释放。孤儿进程不会对系统造成麻烦。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if(0 == pid)
    {
        printf("子进程执行一段时间后退出\n");
        for(int i=0 ;i<25 ;++i)
        {
            printf("pid = %d,ppid = %d\n",getpid(),getppid());
            sleep(1);
        }
        exit(0);
    }
    else
    {
        printf("父进程三秒后退出\n");
        sleep(3);

    }
    return 0;
}

9. 僵尸进程

如果子进程比父进程先结束且父进程还没有对其进行回收与资源释放,那么这个子进程则称为僵尸进程。它会待父进程结束才回收,如果在父进程回收它们之前有过多的僵尸进程产生,则会占用过多的进程号,如果进程号达到最大进程号,则不会产生新的进程,从而影响了系统的工作。所以,僵尸进程会对系统造成麻烦,要避免它的产生。

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

int main()
{
    pid_t pid;
    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if(0 == pid)
    {
        //子进程挂掉,父进程等待25秒后结束,其间子进程是僵尸进程
        printf("子进程结束\n");
        exit(0);
    }
    else
    {
        printf("父进程25秒后结束\n");
        for(int i=0 ;i<25 ;++i)
        {
            printf("父进程do work %d\n",i);
            sleep(1);
        }
    }
    return 0;
}

10. 进程替换 (exec函数族)

exec 是一系列函数,它们可以将进程的程序替换为指定的程序,并从该制定程序的main函数开始执行,达到进程替换的效果。

#include <unistd.h>
extern char **environ;

int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

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

其中只有 execve() 是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

exec 函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。

进程调用一种 exec 函数时,该进程完全由新程序替换,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID (当然还有父进程号、进程组号、当前工作目录……)并未改变。exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段(进程替换)。

exec 函数族与一般的函数不同,exec 函数族中的函数执行成功后不会返回,而且,exec 函数族下面的代码执行不到。只有调用失败了,它们才会返回 -1,失败后从原程序的调用点接着往下执行。

#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello itcast\n");

    char* argv[] = {"ls","-l","/home/itcast",NULL};
    //int ret = execl("/usr/bin/ls","ls","-l",NULL);
    //int ret = execlp("ls","ls","-l",NULL);
    //int ret = execv("/usr/bin/ls",argv);
    int ret = execvp("ls",argv);
    if(-1 == ret)
    {
        perror("execl");
        return 1;
    }

    printf("hello itcast\n");

    return 0;
}

11.进程与文件描述符

在fork之前open一个文件得到文件描述符时,父进程和子进程操作同一个文件,并且文件的信息(偏移量等)会在该文件上更新,更新的信息在文件上,而不是对于一个进程。

在fork之后open文件,两个进程操作的是不同的文件

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    int fd;
    pid_t pid;
    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    fd = open("demo.txt",O_WRONLY | O_CREAT,0664);
    if(-1 == fd)
    {
        perror("open");
        return 1;
    }
    else if(0 == pid)
    {
        const char* buf = "0123456789";
        int ret = write(fd,buf,10);
        if(-1 == ret)
        {
            perror("write");
            return 1;
        }
        close(fd);
    }
    else
    {
        const char* buf = "ABCDEFGHIG";
        int ret = write(fd,buf,10);
        if(-1 == ret)
        {
            perror("write");
            return 1;
        }
        close(fd);
    }
    return 0;
}

无名管道 pipe

无名管道pipe用于有血缘关系的两个进程间的通信,调用pipe函数会打开两个文件描述符,通常使用一个fd[2]数组承接

fd[0] 为读端 ,fd[1] 为写端。

#include <unistd.h>
int pipe(int pipefd[2]);
返回值:
    成功 0
    失败 -1 设置errno
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define SIZE 32

int main()
{
    pid_t pid;
    int fd[2];

    char buf[SIZE];

    int ret = pipe(fd);
    if(-1 == ret)
    {
        perror("pipe");
        return 1;
    }
    printf("管道创建成功:fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);

    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return 1;
    }
    else if(0 == pid)
    {
        //子进程写
        close(fd[0]);

        while(1)
        {
            fgets(buf,SIZE,stdin);
            if('\n' == buf[strlen(buf)-1])
                buf[strlen(buf)-1] = '\0';
            ret = write(fd[1],buf,SIZE);
            if(-1 == ret)
            {
                perror("write");
                return 1;
            }
        }
        close(fd[1]);
    }
    else
    {
        //父进程读
        close(fd[1]);

        while(1)
        {
            memset(buf,0,SIZE);

            ret = read(fd[0],buf,SIZE);
            if(-1 == ret)
            {
                perror("read");
                return 1;
            }
            printf("父进程read:%s\n",buf);
        }
        close(fd[0]);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41775886/article/details/107451640