Linux系统进程管理及相关操作函数

Unix进程的启动顺序
    系统启动进程0,进程0只负责启动进程1(init进程)或启动进程1和进程2;
    其他进程都是由进程1或进程2启动的;
Unix用进程的PID标识进程,PID的本质就是一个整数;
每个进程都有一个唯一的进程ID(PID),在同一时刻进程的PID不会重复;进程的PID可以延时重用;

几个常用函数
    getpid()    取当前进程的PID
    getppid()    取父进程的PID
    getuid()/geteuid()    取当前用户ID(有效用户ID)
    
启动子进程的函数
    fork()        通过复制父进程来启动子进程;
    vfork()+execl()    启动一个全新的子进程,有自己的一套;

fork()创建子进程(简单的复杂函数);
    pid_t    fork(void);
    fork无参,返回PID;
    fork()函数通过复制父进程,创建子进程;会复制除代码区之外的所有内存区域,代码区父子进程共享;
/*
 * fork()函数初探
 */
#include <unistd.h>
#include <stdio.h>
int main() {
    printf("begin\n");
    pid_t pid = fork();
    printf("end, pid = %d\n", pid);
    return 0;
}

    子进程是父进程的拷贝,即子进程从父进程得到了除代码区之外的数据段和堆栈段,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存的方式访问;fork()返回后,父子进程都从调用fork()函数的下一条语句开始执行;
    fork()之前的代码只有父进程执行一次,fork()之后的代码父子进程都各执行一次(执行2次);
    fork()函数有两次返回,父进程返回子进程的PID,子进程返回0;
/*
 * fork()函数返回值可以区分出父子进程
 */
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
int main() {
    printf("begin\n");
    pid_t pid = fork();    //pid可以区分出父子进程;

    //父进程返回子进程的PID,子进程返回0;
    if (pid == -1) {
        perror("fork"), exit(-1);
    }
    //练习父子进程打印不同的内容
    //父进程打印1,子进程打印2
    //在父子进程中打印对方进程的ID;
    if (pid == 0) {
        //父子进程都会做判断,子进程满足条件
        printf("2\n");
        printf("我是子进程%d,父进程是%d\n", getpid(), getppid());
    } else {
        //父进程执行的分支;
        printf("1\n");
        printf("我是父进程%d,子进程是%d\n", getpid(), pid);
    }
    //if/else语句父子进程都执行,但是由于条件不同各自执行的部分不同
    printf("end, pid = %d\n", pid);
    return 0;
}


/*
 * fork()创建的父子进程内存空间相互独立
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int i1 = 10;
int main() {
    int i2 = 10;        //i2子进程复制
    char *str = malloc(10);
    strcpy(str, "abcd");
    pid_t pid = fork();
    printf("pid = %d\n", pid);
    /* 在父进程中返回子进程的PID,在子进程中返回0; */
    if (pid == 0) {
        printf("我是子进程fork()返回%d\n", pid);
    } else {
        printf("我是父进程fork()返回%d\n", pid);
    }
    /* i3父子进程分别创建; */
    int i3 = 10;
    if (pid == 0) {
        /* 子进程执行分支 */
        /* i1,i2子进程会复制 */
        /* i3是父子进程分别创建的,不是复制的 */
        i1 = 20, i2 = 20, i3 = 20;
        str[0] = '1';
        printf("child:i1=%d,i2=%d,i3=%d,str=%s\n", i1, i2, i3, str);
        printf("chaddr:&i1=%p,&i2=%p,&i3=%p,&str=%p\n", &i1, &i2, &i3, str);
        exit(0);
        /* 执行后保证后面的不会执行 */
    }
    sleep(1);
    printf("father:i1=%d,i2=%d,i3=%d,str=%s\n", i1, i2, i3, str);
    printf("faaddr:&i1=%p,&i2=%p,&i3=%p,&str=%p\n", &i1, &i2, &i3, str);
    /* 每个进程都有自己独立的虚拟内存空间; */
    /* 子进程复制了父进程的内存空间,虚拟地址相同,
     * 但属于不同进程 */
    return 0;
}


    fork()函数创建子进程后,父子进程谁先运行不确定,不同的系统有不同的算法;谁先结束也不确定;
    fork()创建子进程时,如果父进程有文件描述符,子进程会复制文件描述符,但不复制文件表;
/*
 * fork()创建子进程时,如果父进程有描述符,
 * 子进程会复制文件描述符,但不复制文件表
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main() {
    //pid_t pid = fork(); //父子进程将分别创建文件描述符
    int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd == -1) {
        perror("open"), exit(-1);
    }
    pid_t pid = fork();    //子进程复制文件描述符
    /* 复制文件描述符,不复制文件表 */
    if (pid == 0) {
        /* 子进程执行分支 */
        printf("child:fd=%d\n", fd);
        write(fd, "abc", 3);
        close(fd);    //关闭子进程的fd
        exit(0);    //执行后保证后面的不会执行
    }
    sleep(1);
    printf("father:fd=%d\n", fd);
    write(fd, "123", 4);
    /* 如果两个文件描述符对应同一个文件表,
     * 文件位置偏移量是同一个,内容不会相互覆盖;
     * 说明没有复制文件表 */
    close(fd);        //关闭父进程的fd
    /* close()只删除对应关系,只有当文件描述符和文件表的对应关系为为0时才删除文件表 */
    return 0;
}


父子进程之间的关系
    fork()之后,父子进程同时运行,如果子进程先结束,子进程给父进程发一个信号,父进程负责回收子进程的资源;
    fork()之后,父子进程同时运行,如果父进程先结束,子进程变成孤儿进程,会认进程1(init进程)做新的父进程;init进程叫孤儿院;
    fork()之后,父子进程同时运行,如果子进程发信号时出现了问题,或者父进程没有及时处理信号,子进程就会变成僵尸进程Z;

fork()出现错误的原因
    1系统进程总数有限额;
    2用户进程总数有限额;
一般情况下,不可能超限额,因此fork()可以不判断-1,fork()出错的后果就是子进程创建失败,父进程继续执行;

练习
    验证如果父进程先结束,子进程会以init进程做新的父进程;
思路
    父进程先sleep(),运行子进程,子进程打印此时的父进程PID,然后子进程再sleep(),父进程结束后,子进程再次打印父进程的PID;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
    printf("父进程pid=%d\n", getpid());
    pid_t pid = fork();
    if (pid == 0) {
        printf("父进程PID=%d\n", getppid());
        sleep(2);
        printf("父进程PID=%d\n", getppid());
    } else {
        printf("pid = %d\n", pid);
        sleep(1);
        printf("父进程%d结束\n", getpid());
        /* ./a.out的父进程是bash的子进程,
         * 结束后bash就会打印一次终端提示符 */
    }
    return 0;
}


进程的结束/终止
进程可能是正常结束也可能是非正常结束;
进程终止的5种正常情况
    1> 主函数main()中执行 return 语句;
    2> 函数 exit()可以终止进程;
    3> _exit()/_Exit()可以终止进程;
    4> 进程的最后一个线程执行了return返回语句(结束);
    5> 进程的最后一个线程调用pthread_exit()函数(结束);
非正常结束进程的方法
    1> 被信号终止;
    2> 主线程被其他线程取消;

return 和 exit()区别:
    return 是用来退出函数的;
    exit() 是用来退出进程的;

exit()/_exit()/_Exit()的区别
    The function _exit() terminates the calling process "immediately".
    exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中;
    exit()被调用时不会立即结束进程,会先调用exit_handler并调用atexit()/on_exit()注册过的函数之后再结束进程;
    _exit()/_Exit()在底层是一样的,没有区别; _Exit()立即结束进程;
    _exit()函数只为进程实施内核清理工作;子进程中最好用_exit(),而exit()会影响父进程的状态;
    
    atexit(函数指针)允许进程在结束之前调用其他函数,但如果用_Exit()结束进程时不调用;
int atexit(void (*func)(void))
    可以注册多个函数,一个函数也可注册多次,按FILO顺序执行;
    函数注册后,无法取消注册;
    在清理函数中调用exit()在Linux中会继续执行剩下的清理函数但在某些系统可能出现死循环;
    子进程会继承父进程的清理函数;

/*
 * atexit()/exit()演示
 */
#include <stdio.h>
#include <stdlib.h>
void func1() {
    printf("func1 is called\n");
}
void func2() {
    printf("回收资源,善后处理\n");
}
int main() {
    atexit(func1);        //只注册func1(),只有在exit()时才调用;
    atexit(func2);        //atexit()的参数是函数指针,可以多次调用;
    printf("begin\n");
    /* exit()不会立即结束,可以调用atexit()注册过的函数再结束进程; */
    exit(0);        //参数是退出码,可以用来记录退出情况;
    _Exit(0);        //立即结束,不掉用fa;
    printf("end\n");
    return 0;
}

exit(0);//如何取退出码?
The exit() function causes normal process termination and the value of status & 0377 is returned to the parent (see wait(2)).
退出码返回给父进程;必须保证子进程先结束,父进程才能拿退出码;
函数wait()/waitpid()用于让父进程等待子进程的结束,并取得子进程的退出信息;
wait()/waitpid()就是让父进程等待子进程
    1-> 如果所有子进程都在运行,父进程阻塞;
    2-> 如果有一个子进程结束,父进程取得子进程的退出信息并返回,父进程继续运行;
    3-> 如果没有子进程,父进程直接返回,继续运行;
    注:僵尸子进程用wait()/waitpid()回收;僵尸进程是已经终止但资源没有被回收的进程;所以wait()/waitpid()函数又叫殓(lian)尸工;
    如果一个子进程已经终止并且是僵尸进程,wait()会立即返回并取得该子进程的状态,否则阻塞;
二者的区别是
    在一个子进程终止前,wait()使其调用者阻塞,而waitpid()提供更多选择;

pid_t wait(int *status);//参数是status的地址
    wait()会让父进程等待任意子进程的结束,返回结束的子进程的PID,并把子进程的退出信息(是否正常退出和退出码)放入参数status参数中;
    是否正常退出可以用WIFEXITED(status)判断,
    退出码用WEXITSTATUS(status)获取0-255;
/*
 * wait()函数演示
 */
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
    printf("current pid = %d\n", getpid());
    pid_t pid = fork();    //pid只是fork的返回值
    if (pid == 0) {
        /* 子进程执行的分支 */
        printf("child pid = %d\n", getpid());
        sleep(1);
        printf("child end\n");
        exit(10);
    }
    int stat;
    pid_t wpid = wait(&stat);
    /* 等待任意一个子进程的结束,
     * 参数为子进程退出码的地址
     * 返回等到的子进程的PID */
    printf("father go on\n");
    printf("endchild pid = %d\n", wpid);
    /* 判断子进程是否正常将结束 */
    if (WIFEXITED(stat)) {
        /* stat不能是指针,只能是数 */
        printf("进程是正常结束\n");
        /* 返回子进程的退出码 */
        printf("exit code: %d\n", WEXITSTATUS(stat));
    }
    return 0;
}


pid_t waitpid(pid_t pid, int *status, int options);
    可以等待指定的子进程;
    在等待的过程中父进程可以阻塞也可以不阻塞;
参数
    pid可以指定等待哪个/哪些子进程;
    options可以指定等待时是否阻塞;
pid的值
    -1,等待任意一个子进程的结束;
    >0,等待特定的子进程结束(子进程PID=pid);
    0,等待本进程组的子进程结束;
    <-1,等待进程组ID等于pid绝对值的子进程;
options的值
    0代表阻塞;
    WNOHANG代表非阻塞;如果options用了WNOHANG,返回有三种
        正数    等待到结束的子进程PID;
        0        没有子进程结束,直接返回;
        -1        出错了;
exit()不论参数是多少,都是正常结束进程,但一般用负数表示没有完成相关的功能;
/*
 * waitpid()函数练习
 * 一个父进程创建两个子进程
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
    pid_t pid1 = fork();
    pid_t pid2;
    if (pid1 > 0) {
        /* 父进程创建第2个子进程; */
        pid2 = fork();
    }
    if (pid1 == 0) {
        /* 子进程1执行的分支 */
        printf("pid1 = %d\n", getpid());
        sleep(1);
        printf("pid1 end\n");
        exit(-1);
    }
    if (pid2 == 0) {
        /* 子进程2执行的分支 */
        printf("pid2 = %d\n", getpid());
        sleep(3);
        printf("pid2 end\n");
        exit(20);
    }
    int stat;
    /* 在父进程中pid2是第2个子进程的pid,不是0; */
    pid_t wpid = waitpid(pid1 /*-1*/ , &stat, 0);
    /* -1等待任意一个子进程,>0等待特定的子进程; */
    /* 0代表阻塞, WNOHANG代表非阻塞; */
    printf("father end\n");
    printf("wpid = %d\n", wpid);
    if (WIFEXITED(stat)) {
        printf("进程正常结束\n");
        printf("exit code: %d\n", WEXITSTATUS(stat));
    }
    printf("end\n");
    return 0;
}


wait()和waitpid()的区别
    wait()是等待任意一个子进程的结束,等待过程必然阻塞;
    waitpid()可以等待指定的子进程结束(也可以任意),等待过程中父进程可以阻塞也可以不阻塞;


创建子进程的两种方式
    1-> fork()    复制父进程,创建子进程;
    2-> vfork()+execl()    创建全新的子进程;
pid_t vfork(void);
    create a child process and block parent;
    vfork()从语法上看,和fork()一样;但机制和fork()完全不同;
    vfork()不会复制父进程的任何资源;子进程会占用父进程的资源运行,父进程阻塞;直到子进程调用exec系列函数(比如:execl())或者子进程结束,资源就会还给父进程,解除父进程阻塞;
    用execl()函数可以让父子进程并行;

    vfork()函数只能创建新进程,但不提供程序;
    execl()只提供程序,不创建新的进程;
    vfork()+execl()既有进程,又有执行的程序;

fork()和vfork()函数的区别
    fork()要拷贝父进程的数据段;而vfork()不需要完全拷贝父进程的数据段,在子进程没有调用exec和exit()之前,子进程与父进程共享数据段;
    fork()不对父子进程的执行次序进行任何限制;而在vfork()调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit()之后,父子进程的执行次序才不再有限制;

    vfork()创建的子进程一定先于父进程运行,直到子进程运行了execl()函数才能同时运行;
    vfork()创建的子进程,如果不调用exec系列函数,必须用exit()强行退出,否则死循环;
    若使用vfork()在调用exec系列函数后,还要使用exit()函数,防止exec系列函数启动失败导致的死循环;
/*
 * vfork函数演示
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
    /* pid_t pid = fork(); */
    pid_t pid = vfork();
    /* vfork()创建的子进程会阻塞父进程并占用父进程资源 */
    if (pid == 0) {
        /* 子进程执行的分支; */
        sleep(1);
        printf("child\n");
        /* vfork()创建的子进程,如果不调用exec系列函数,
         * 必须用exit()强行退出,否则死循环; */
        exit(0);
    }
    printf("father\n");
    return 0;
}


execl()函数负责启动一个全新的程序;
格式
int execl(const char *path, const char *arg, ...);
    第1个参数是程序的全路径(路径/程序名),一定不能错;
    第2个参数是执行程序的命令(可以错),
    后面还可以跟可选参数,一般是选项或参数等,最后以NULL结束;
    失败返回-1,失败就意味着没有启动新程序;
    如果启动成功直接启动命令并退出,后面的语句不执行;
    注:execl()函数不会改变进程的PID,只会改变进程执行的代码(全新的程序);
/*
 * execl()函数演示
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    printf("begin\n");
    int res = 0;
    /* 路径如果错误,启动失败;命令错误没有关系; */
    res = execl("/bin/ls", "ls", NULL);
    /* res = execl("/bin/ls", "ls", "../", NULL); */
    /* res = execl("/bin/ls", "ls", "-al", "../", NULL); */
    /* 第1个参数表示程序路径,第2个参数就是命令; */
    /* 后面是可选参数; */
    /* 最后一个参数要为NULL; */
    printf("res = %d\n", res);
    printf("end\n");
    return 0;
}


vfork()+execl()练习
/*
 * vfork保证子进程先运行,
 * 子进程执行到execl时父子进程同时运行;
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
    pid_t pid = vfork();
    if (pid == 0) {
        /* 子进程执行的分支; */
        sleep(1);
        execl("/bin/pwd", "pwd", NULL);    //立刻将资源还给父进程;
        printf("child execl() failed\n");
        exit(0);    //防止execl启动失败导致的死循环;
    }
    printf("father\n");
    return 0;
}


验证execl()不启动新进程,只提供程序;
思路
    用vfork()创建子进程,在调用execl()函数之前打印vfork()创建的子进程PID,在execl()调用程序中打印一些子进程的PID,如果两个相等,意味着execl()没有创建新的子进程;
    打印PID的子进程的程序自己写一个;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
    printf("current pid = %d\n", getpid());
    pid_t pid = vfork();
    if (pid == 0) {
        /* 子进程执行的分支; */
        //sleep(1);
        printf("main child pid = %d\n", getpid());    //获取进程PID
        printf("马上执行execl()\n");
        /* 首先编写打印自己pid的程序并编译为printpid.o */
        execl("./printpid.o", "printpid", NULL); //立刻将资源还给父进程;
        exit(0);    //防止execl启动失败导致的死循环;
    }
    printf("father is running ...\n");
    waitpid(pid, NULL, 0);    //让父进程等待子进程结束,防止僵尸进程
    printf("father go on\n");
    return 0;
}

/*
 * 打印自己pid的函数
 * gcc 07printpid.c -o printpid.o
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    printf("我是execl中调用的子函数pid = %d\n", getpid());
    return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值