为linux添加一个系统调用
内核版本:linux-3.13.11 32位内核
简介:
这是我的linux课大作业,参考了许多篇csdn的博客,获益良多,同时也踩了不少坑,现在课程分数已经出了,所以把课程论文和代码开源,希望能帮助到后来人。
第一次发博客,加上本身水平实在有限,如有问题可在评论区讨论。
同时,文中抄了几段话,已经忘记抄的是哪几篇博客,如果有侵权,请联系我删除。
一、设计目标
设计一个系统调用并将其编译进linux内核,利用该系统调用实现类似于pstree命令实现的效果,即在控制台打印出进程PID及进程间的父子兄弟关系。
二、主要设计思路
该程序需要利用内核权限遍历进程,获取到进程的pid以及各进程之间的关系,所以程序必须运行在内核态,并且需要很高的权限才能将该系统调用装入linux内核,有两种方法实现:
1.内核模块法
利用内核模块的高权限(运行在内核态),“篡改” 系统调用表,直接将我们的系统调用函数 “注入” 到内核中,实现系统调用的添加,由于该方法不用编译内核,所以可以快速实现,适合用于前期的系统调用debug阶段,快速进行系统调用程序的调试。
2.编译内核法
直接将系统调用编译进linux内核。属于最终的系统调用固化方法,由于该方法需要编译内核所以速度较慢。
最终思路:
由于我们需要长期稳定使用该系统调用,且编译内核能更多的学习关于内核的知识,所以选择编译内核法将系统调用装入内核。但是由于编译一次太慢,会导致调试效率低下,所以先利用内核模块法将我们编写的系统调用程序测试稳定后,再编译进内核。
三、系统调用的原理实现以及系统调用验证方法
1、系统调用原理
用户空间程序无法直接执行,所以应用程序需要用某种方式通知系统,告诉内核需要一个系统调用,希望跳转到内核态,这时内核就可以代表应用程序在内核态运行系统调用。
通知内核靠的是软中断:通过引发一个异常来促使系统切换带内核态执行异常处理程序,此时异常处理程序实际就是系统调用处理程序,这样就可以陷入内核,但是还需要把系统调用号一并传给内核。在X86上,预定义的软中断的中断号是128,通过int$0x80
指令触发该终端,该指令会触发一个异常导致系统切换到内核态并执行128号异常处理程序system__call()
,系统调用号通过eax寄存器传递给内核,system_call()
通过将调用号与NR_syscalls作比较来检测其有效性。如果大于等于NR_syscalls,则函数返回-ENOSYS
,否则执行系统调用:call *sys_call_table(,%rax,8)
,由于系统调用表中的表项以64位存放,所以内核需要将给定的调用号*8,然后用所得结果在该表中查询其位置。
2、系统调用程序设计思路
要实现的系统调用基本功能为遍历linux进程树,并记录当前进程的遍历深度和有无更年轻的兄弟进程等信息。在linux内核中,利用的是task_struct
结构体来实现进程的各种信息的记录,所以我们要做的就是遍历task_struct
结构体,并记录相关信息。
linux进程之间的关系有父子关系和兄弟关系,在task_struct
结构体中,父子关系是利用多叉树实现的,而一个进程的兄弟关系,是利用了链表来表示。
在task_struct
结构体中,使用chindren
和sibling
成员来实现。其中关系如下(图中sibling.next有错误,具体请看第六章):
可见,linux中进程之间是以多叉树为关系进行组织的。这里我们采用前序遍历的方法,对该多叉树进行遍历。结合上图可知,我们可以利用sibling.next
进行同辈的遍历,利用children
进行深度的遍历。
遍历得到的进程信息采用结构体进行存储:
struct process //保存得到的进程信息
{
int pid; //当前进程PID
int depth; //当前深度
int have_brother; //还有无年纪更小的兄弟进程
char name[16]; //进程名
};
struct process a[1000];
其中 have_brother
变量是为了绘图的时候确定兄弟关系什么时候截止来定的,若没有这个成员变量,则难以界定表明兄弟关系线绘制到哪里停止。
由于linux的进程树为一颗多叉树,且我们想要表现出从root到顶端的层级关系,所以用深度搜索中的前向遍历比较合适,多叉树的前向遍历类似于二叉树的前向遍历,只是在进行递归的同时需要遍历多个兄弟进程。
进程多叉树遍历部分代码如下:
/**
*递归遍历linux进程树
*linux进程树本质上是一个N叉树,为了方便显示进程间的父子关系,采用前序遍历
*兄弟进程通过链表的形式相互连接,所以还需要遍历链表,此处采用list_for_each
*在内核源码中,list_for_each实际上就是一个宏定义,封装了一个for循环
*/
void preorder_traversal_processtree(struct task_struct *p_task,int b)
{
struct list_head *list; //兄弟链表
a[counter].pid = p_task -> pid;
a[counter].depth = b; //记录当前遍历深度
//用以确定是否为链表中最后一个兄弟进程
if(p_task -> sibling.next == &(p_task->parent->children))
a[counter].have_brother=0;
else
a[counter].have_brother=1;
存储当前进程的进程名
strcpy(a[counter].name,p_task -> comm);
counter ++;
list_for_each(list, &p_task->children)//遍历兄弟进程并进行递归
{
struct task_struct *t = list_entry(list, struct task_struct, sibling);
preorder_traversal_processtree(t,b+1);
}
}
判断最后一个兄弟进程的时候利用的是兄弟进程双向链表来判断。当遍历到了最后一个兄弟时,下一个兄弟会是第一个兄弟(父进程的第一个子进程)。
遍历兄弟进程时,没有直接使用for循环,而是使用了list_for_each
。其实list_for_each
就是linux内核为了简便统一而封装的链表遍历函数(实际是一个宏定义for循环),其源码如下:
/**
* list_for_each - iterate over a list
* @pos: the &struct list_head to use as a loop cursor.
* @head: the head for your list.
*/
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
至于为什么遍历的head传入&p_task->children
,是因为children->next
实际指向的是子进程的sibling
成员,即链表头。
list_entry
是一个很有意思的宏函数,它存在的意义是利用编译原理找“儿子的父亲”即找成员所在的结构体的地址,使用它是因为在linux内核中为了能复用代码,树和链表使用的结构体内部是不包含数据的,而是将结构体直接嵌在母结构体中,在母结构体中放入数据。这就导致在递归遍历时不能直接获取到当前结构体所对应的数据,所以需要使用list_entry
来获取其母结构体的地址,从而获取到数据。
系统调用的主函数为:
/**
*系统调用的主体
*将root进程传入递归函数,进行递归遍历
*由于内核和用户之间的数据不能直接互换,需要通过copy_to_user
*把遍历得到的进程树据交换到用户空间,类似于内核和用户之间的memcpy();
*/
asmlinkage long my_syscall(char __user * buf)
{
counter = 0;
preorder_traversal_processtree(&init_task,0); //进行递归遍历
copy_to_user((struct process *)buf,a,1000*sizeof(struct process)) //传出数据
}
其中参数 char __user * buf
为想要向用户态传送的数据。
该函数为系统调用的入口,即进行系统调用时,陷入内核后,根据系统调用号会跳转到这个函数执行。该函数实际调用了上面提到的进程多叉树遍历函数,其中&init_task
为根进程的task_struct
,即init
进程。该进程为所有进程的父进程,位于二叉树的树干。将该进程的task_struct
传入遍历函数,触发递归。
copy_to_user()
可以理解为内核与用户之间的memcpy()
,由于内核与用户之间不共享内存,所以运行于内核态的系统调用无法将获取到的进程数据直接传送给用户,只能通过copy_to_user()
来实现。
3、利用内核模块“注入”系统调用进行调试
为了加快代码调试速度,当然不能每次修改系统调用代码后都重新编译内核。所以可以采用一点小技巧来临时将系统调用装入内核,这个小技巧则是利用内核模块。
由于内核模块运行在内核态,所以其拥有极高的权限,我们可以利用该权限,将系统调用临时替换到系统调用表中的空位置,达到快速调试代码,不用重新编译内核的目的。
可以写一个内核模块,在insmod
该模块时,该模块将系统调用注入,当rmmod
时,该模块将系统调用恢复原状。一个内核模块最基本的就是加载和移除函数,如下:
/*
*内核模块初始化函数
*insmod时被调用
*/
static int __init mymod_init(void)
{
insert_syscall();
return 0;
}
/*
*内核模块卸载函数
*rmmod时被调用
*/
static void __exit mymod_exit(void)//移除内核模块函数
{
remove_syscall();
}
当执行insmod
时,insert_syscall();
则会被调起,该函数的目的就是修改系统调用表,将系统调用表的指定位置指向我们自己写的系统调用函数。
在修改系统调用表之前,需要先获取系统调用表的地址,和哪些系统调用号暂时闲置。利用cat /usr/include/asm-generic/unistd.h
查看闲置系统调用号,利用grep sys_call_table /boot/System.map-3.13.11
查看系统调用表的地址。得到的结果如下:
#define free_syscall_num 222 //当前空闲的系统调用号
#define syscall_table_address 0xc1655140 //系统调用表地址
接下来就可以插入系统调用了,insert_syscall()
如下:
/*
*插入系统调用
*保存原位置的系统调用,一般为sys_ni_syscall()
*清除写保护,插入我们自己的系统调用
*使能写保护,恢复原状
*/
void insert_syscall(void)
{
syscall_table = (uint32_t *)syscall_table_address;
save_syscall = (long(*)(void))(syscall_table[free_syscall_num]);
disable_wirte_protect();
syscall_table[free_syscall_num] = &my_syscall;
enable_wirte_protect();
}
这个函数执行的过程:
- 保存系统调用表中该位置处原本指向的函数。
- 关闭写保护(WP)(允许改写系统调用表)
- 将系统调用表中该位置指向我们自己写的系统调用函数。
- 开启写保护(WP)
关闭和开启写保护的原理如下:
/*
*通过内联汇编的方式,清除CR0寄存器的第17位“WP”(写保护位)
*清除后,原本只读的系统调用表就可以变为可读写
*/
void disable_wirte_protect(void)
{
uint32_t cr0=0;
asm volatile("movl %%cr0,%0":"=r"(cr0)::);
cr0 &=0xfffeffff;
asm volatile("movl %%eax, %%cr0"::"a"(cr0));
}
/*
*通过内联汇编的方式,置位CR0寄存器的第17位“WP”(写保护位)
*置位后,原本可读写的系统调用表就可以变为只读,恢复了原状
*/
void enable_wirte_protect(void)
{
uint32_t cr0=0;
asm volatile("movl %%cr0,%0":"=r"(cr0)::);
cr0 |=0x00010000;
asm volatile("movl %%eax, %%cr0"::"a"(cr0));
}
通过内联操纵CR0寄存器的第16位(WP),当该位置置0,写保护就被关闭,反之则开启,所以可以通过内联汇编,将CR0寄存器读回后将WP位置0再写回CR0来关闭写保护,反之开启写保护。内联汇编的格式为:
asm volatile(
"Instruction List"
:Output
:Input
:Clobber/Modify
);
按照该格式编写即可。
移除系统调用的过程与插入系统调用的过程类似:
void remove_syscall(void)
{
disable_wirte_protect();
syscall_table[free_syscall_num] = save_syscall;
enable_wirte_protect();
}
只需关闭写保护后将系统调用表的指向恢复原状,再开启写保护即可完成。
4、测试代码的编写
- 用户程序—树状图构建程序的编写
我们的最终目标时构建出一个树状图。这个图的数据源是结构体数组a
struct process
{
int pid;
int depth;
int have_brother;
char name[16];
};
struct process a[1000];
我们利用系统调用传回的数组,构建出linux进程树图,表现出进程之间的父子兄弟关系。代码如下(mymod_test.c):
int main()
{
int i,j;
syscall(222,&a);
printf("%d\n",a[0].pid);
for(i = 1; i < 1000; i++)
{
if(a[i].depth>a[i-1].depth)
{
if(brother_reg[a[i].depth]==0&&a[i].have_brother==0)
printf("\t──");
if(brother_reg[a[i].depth]==0&&a[i].have_brother==1)
printf("\t┬─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==0)
printf("\t└─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==1)
printf("\t├─");
printf("─%d",a[i].pid);
}
else
{
printf("\n");
for(j=0;j<a[i].depth-1;j++)
{
if(brother_reg[j+1]==0)
printf("\t");
else
printf("\t│");
}
printf("\t");
if(brother_reg[a[i].depth]==0&&a[i].have_brother==0)
printf("──");
if(brother_reg[a[i].depth]==0&&a[i].have_brother==1)
printf("┬─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==0)
printf("└─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==1)
printf("├─");
printf("─%d",a[i].pid);
}
brother_reg[a[i].depth] = a[i].have_brother;
if(a[i+1].pid == 0)
break;
}
printf("\n");
return 0;
}
基本思路为根据不同的条件(比如是否为父子关系链中最小的,是否为最后一个兄弟),绘制不同的字符── ┬─ └─├─
,以实现树状图的绘制。这类字符是从pstree命令中采集出的,优点是相互拼接无缝隙且格式整齐。
其中syscall(222,&a);
便是系统函数的调用函数,这个函数内部的实现是首先系统调用号传入eax
寄存器,然后触发0x80号软中断,在中断中读取eax
寄存器,从而知道调起的是哪个系统调用。
- 测试程序的编写:
编写一个测试程序,运行该程序可以产生一个设计好的进程树,这时可以使用我们实现的系统调用来观察是否能正确显示该进程树。测试程序的进程树设计如下:
代码如下(setprocess.c):
int main()
{
int p1,p2,p3;
while ((p1=fork())==-1);
if(p1==0)
{
while ((p2=fork())==-1);
if(p2==0)
{
while ((p3=fork())==-1);
if(p3==0)
{
printf("I am D,my pid is %d,my parent's pid is %d\n",getpid(),getppid());
}
else
{
printf("I am C,my pid is %d,my parent's pid is %d\n",getpid(),getppid());
}
}
else
{
printf("I am B,my pid is %d,my parent's pid is %d\n",getpid(),getppid());
}
}
else
{
printf("I am A,my pid is %d\n",getpid());
}
getchar();
}
3.Makefile的编写
KVERS = $(shell uname -r)
# Kernel modules
obj-m += mymod.o
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
内核模块的makefile形式较为固定,其中obj +=mymod.o
的意思是将mymod.o
转化成一个内核模块mymod.ko
,而mymod.o
的来源是mymod.c
,虽然没有显式地定义并生成mymod.o
,但是make有强大的自动推导功能,它可以自动推导出所需依赖。
KVERS = $(shell uname -r)
为变量的定义,其保存了内核的版本,类似于c语言中的宏定义,属于字符串替换。
其中build kernal_modules clean
均为伪目标,它们不对应目标文件,只是为了运行指定的指令。build
伪目标是一种常用的技巧,利用排在第一位的伪目标,可以实现多个文件的生成,例如可以把测试文件的生成目标也放在这个伪目标里,这样只需要敲一下make就可以同时生成系统调用和测试程序。
四、系统调用的测试:
1.程序编译方法
- 编译测试进程生成程序(setprocess.c)
gcc -o setprocess setprocess.c
- 编译树状图生成程序(mymod_test.c)
gcc -o mymod_test mymod_test.c
- 编译内核模块(系统调用)程序(mymod.c)
make
2.程序运行方法和运行结果
- 运行
setprocess.o
生成测试进程./setprocess.o
- 装载内核模块
sudo insmod mymod.ko
- 运行测试程序
mymod_test.o
./mymod_test.o
可见,符合设计预期,显示测试进程完全正确,且显示格式规整(与pstree非常相似),但是因为水平有限,无法显示进程名的同时显示规整,所以只显示pid不显示进程名。
可见,linux下所有进程都是从init进程开始的,所以进程树的树根pid为0(init进程)。利用这种设计思路,设计出的进程树显示程序显示美观,可以清晰地表现出进程之间的层级关系。
且由于使用了内核模块法进行系统调用程序的调试,该程序debug非常快捷方便,至此,该程序已无可察觉的bug,可以进入到内核编译的步骤。
五、将系统调用编译进内核
1.下载解压linux内核
选择的是linux-3.13.11
32位的内核
2.添加系统调用号:
sudo vim /usr/src/linux-3.13.11/arch/x86/syscalls/syscall_32.tbl
将我们自己写的系统调用加在351号处
3.添加系统调用函数声明(不能放在宏定义里):
sudo vim /usr/src/linux-3.13.11/include/linux/syscalls.h
4.添加函数实现(放在最后的#endif以外)
sudo vim /usr/src/linux-3.13.11/kernel/sys.c
5.编译内核和安装
流程如下:
cd /usr/src/linux-4.16.1/
apt update
apt install libncurses5-dev
apt install build-essential
apt install libssl-dev
apt install libelf-dev
apt install bison
apt-get install bison -y
apt install flex
make mrproper
make clean
make menuconfig
make -j16
make modules
make modules_install
make install
update-grub2
reboot -n
make modules_install
make install
此时重启后,即可查看
六、出现的问题以及解决办法
1.make -j16 内核时,报错。
查看报错后,感觉是函数未声明,所以就进入sudo vim /usr/src/linux-3.13.11/include/linux/syscalls.h
查看,发现我把我自己写的sys_mymod
加在了一个宏定义里,导致编译不到。把函数声明拿出来,和getpid等会被编译的系统调用放在一起,然后重新make,问题就解决了。
2.sibling.next问题
设计系统调用时,想要获取一个进程有没有更小的兄弟进程,从网上多方查找,都说当自己为最小的兄弟进程,sibling.next
将会指向父进程。可是这样设计出来根本不能得到正确的数据。由于网上到处都是这样说的,所以更加相信这样没问题,但是浪费一晚上尝试各种办法后,均无法得到正确数据。实在没办法了,就在睡前胡乱尝试,将sibling.next
当作指向父进程的子进程(最大的兄弟进程)来处理,结果一试便成功。
七、程序运行结果以及使用说明
内核编译并启动完成后,系统调用便固化在内何中,此时第351号系统调用就是mymod,所以直接使用测试程序调用即可。但是不同的是,测试程序(mymod_test.c)需要修改,将222改为351.
然后,先生成测试进程./setprocess
,最后运行测试程序./mymod_test
结果如下:
可见测试进程PID和关系被正确显示了出来.
完整代码:
Makefile:
KVERS = $(shell uname -r)
# Kernel modules
obj-m += mymod.o
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
mymod.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/unistd.h>
#include <asm/uaccess.h>
#include <linux/sched.h>
#define free_syscall_num 222 //当前空闲的系统调用号
#define syscall_table_address 0xc1655140 //系统调用表地址
long(*save_syscall)(void); //用来保存原本的sys_ni_syscall();
uint32_t *syscall_table=0; //保存系统调用表的地址
struct process //保存得到的进程信息
{
int pid;
int depth;
int have_brother;
char name[16];
};
struct process a[1000];
int counter = 0;
/*
*通过内联汇编的方式,清除CR0寄存器的第17位“WP”(写保护位)
*清除后,原本只读的系统调用表就可以变为可读写
*/
void disable_wirte_protect(void)
{
uint32_t cr0=0;
asm volatile("movl %%cr0,%0":"=r"(cr0)::); //读取CR0
cr0 &=0xfffeffff; //WP位置0
asm volatile("movl %%eax, %%cr0"::"a"(cr0));//写入CR0
}
/*
*通过内联汇编的方式,置位CR0寄存器的第17位“WP”(写保护位)
*置位后,原本可读写的系统调用表就可以变为只读,恢复了原状
*/
void enable_wirte_protect(void)
{
uint32_t cr0=0;
asm volatile("movl %%cr0,%0":"=r"(cr0)::);
cr0 |=0x00010000; //WP位置1
asm volatile("movl %%eax, %%cr0"::"a"(cr0));
}
/*
*递归遍历linux进程树
*linux进程树本质上是一个N叉树,为了方便显示进程间的父子关系,采用前序遍历
*兄弟进程通过链表的形式相互连接,所以还需要遍历链表,此处采用list_for_each
*在内核源码中,list_for_each实际上就是一个宏定义,封装了一个for循环
*/
void preorder_traversal_processtree(struct task_struct *p_task,int b)
{
struct list_head *list;
a[counter].pid = p_task -> pid;
a[counter].depth = b;
if(p_task -> sibling.next == &(p_task->parent->children)) //判断有无更小的兄弟进程
a[counter].have_brother=0;
else
a[counter].have_brother=1;
strcpy(a[counter].name,p_task -> comm); //存储PID名
counter ++;
list_for_each(list, &p_task->children) //进行深度搜索
{
struct task_struct *t = list_entry(list, struct task_struct, sibling); //把sibling对应的task_struct找出来
preorder_traversal_processtree(t,b+1); //进行下一轮深度搜索
}
}
/*
*系统调用的主体
*将root进程传入递归函数,进行递归遍历
*由于内核和用户之间的数据不能直接互换,需要通过copy_to_user
*把遍历得到的进程树据交换到用户空间,类似于内核和用户之间的memcpy();
*/
asmlinkage long my_syscall(char __user * buf)
{
counter = 0;
preorder_traversal_processtree(&init_task,0); //从根进程开始深度遍历多叉树
if(copy_to_user((struct process *)buf,a,1000*sizeof(struct process))) //将数据传回用户空间
return -EFAULT;
else
return sizeof(a);
}
/*
*插入系统调用
*保存原位置的系统调用,一般为sys_ni_syscall()
*清除写保护,插入我们自己的系统调用
*使能写保护,恢复原状
*/
void insert_syscall(void)
{
syscall_table = (uint32_t *)syscall_table_address; //地址强制转换为指针
save_syscall = (long(*)(void))(syscall_table[free_syscall_num]);//保存该位置原来的系统调用
disable_wirte_protect(); //关闭写保护(WP)
syscall_table[free_syscall_num] = &my_syscall; //把该位置的系统调用替换为我们自己写的函数
enable_wirte_protect(); //恢复写保护
}
/*
*移除系统调用
*清除写保护,并把sys_ni_syscall写回原位置
*恢复写保护
*/
void remove_syscall(void)
{
disable_wirte_protect();
syscall_table[free_syscall_num] = save_syscall;
enable_wirte_protect();
}
/*
*内核模块初始化函数
*insmod时被调用
*/
static int __init mymod_init(void)
{
insert_syscall();
return 0;
}
/*
*内核模块卸载函数
*rmmod时被调用
*/
static void __exit mymod_exit(void)//移除内核模块函数
{
remove_syscall();
}
module_init(mymod_init); //内核模块初始化函数回调函数注册
module_exit(mymod_exit); //内核模块卸载函数回调函数注册
MODULE_LICENSE("GPL");
mymod_test.c:
#include <linux/unistd.h>
#include <syscall.h>
#include <sys/types.h>
#include <stdio.h>
struct process
{
int pid;
int depth; //遍历深度
int have_brother;//存储该进程有无更小的兄弟进程
char name[16]; //存储pid名,暂时不用
};
struct process a[1000];
int brother_reg[100]; //存储在某一遍历深度下,还有无更小的子进程
int main()
{
int i,j;
syscall(222,&a); //测试内核模块法时系统调用号为222,测试编译内核法时系统调用号改为351
printf("%d\n",a[0].pid); //先打印出root进程pid(0)
for(i = 1; i < 1000; i++)
{
if(a[i].depth>a[i-1].depth) //判断自己是不是上一个进程的子进程,决定怎么绘图
{
//该是上一个进程的子进程,不换行,在上个进程号后绘制叠加
if(brother_reg[a[i].depth]==0&&a[i].have_brother==0)
printf("\t──");
if(brother_reg[a[i].depth]==0&&a[i].have_brother==1)
printf("\t┬─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==0)
printf("\t└─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==1)
printf("\t├─");
printf("─%d",a[i].pid);
}
else //不是上一个进程的子进程
{
printf("\n"); //换行,绘制兄弟或者非兄弟
for(j=0;j<a[i].depth-1;j++)
{
if(brother_reg[j+1]==0)
printf("\t");
else
printf("\t│");
}
printf("\t");
if(brother_reg[a[i].depth]==0&&a[i].have_brother==0)
printf("──");
if(brother_reg[a[i].depth]==0&&a[i].have_brother==1)
printf("┬─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==0)
printf("└─");
if(brother_reg[a[i].depth]==1&&a[i].have_brother==1)
printf("├─");
printf("─%d",a[i].pid);
}
brother_reg[a[i].depth] = a[i].have_brother; //存储每个深度下有无后续的兄弟,没有的话需要改变绘图策略
if(a[i+1].pid == 0)
break;
}
printf("\n");
return 0;
}
setprocess.c:
#include<stdio.h>
#include<stdlib.h>
///生成一个进程树,结构是A->B->C->D一条链
int main()
{
int p1,p2,p3;
while ((p1=fork())==-1);
if(p1==0)
{
while ((p2=fork())==-1);
if(p2==0)
{
while ((p3=fork())==-1);
if(p3==0)
{
printf("I am D,my pid is %d,my parent's pid is %d\n",getpid(),getppid());
}
else
{
printf("I am C,my pid is %d,my parent's pid is %d\n",getpid(),getppid());
}
}
else
{
printf("I am B,my pid is %d,my parent's pid is %d\n",getpid(),getppid());
}
}
else
{
printf("I am A,my pid is %d\n",getpid());
}
getchar();
}