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

时间:2021-01-17 20:32:38
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;
}