UNIX系统编程(1)
注:本文来自“网易”博主,仅阅读,学习
第一章:什么是系统编程
UNIX系统编程,简单的说就是“C语言+系统调用(system call)”,学会了C语言再知道一些系统调用的方法,其实就可以进行UNIX系统编程了。那什么又是系统调用呢,其实初学者就把它看当成是函数用就可以了。这些“函数”是干什么用的呢,大家知道操作系统内核管理着我们的计算机资源,比如CPU,内存,硬盘等等。应用程序是无法直接访问到它们的。那我们想利用这些资源怎么办呢,内核就给我们提供了一个接口,我们可以利用这个接口来进行计算机资源的使用。内核也通过接口来判断我们的使用请求是否合法,合法的的提供资源,不合法的给与干掉。就好比是金库,银行和储户。金库里有要多地人民币,这就是资源。然而我们储户却无法直接接触到这些可爱的人民币,因为它们是通过银行来管理的,银行就好比是内核。但我们怎么样才能从金库里取出钱来呢,我们可以去银行窗口办理存款取款手续,这就是系统调用。当然,每个人的取款限额都不一样,大款的存款多,他可以取几百万甚至更多,而我存款少,取出一万块就已经不错了。这就是用户的系统调用权限不同。还有就是银行行长,他对这个金库的权限更大(root),当然他的责任也更大,他的一个错误决定有可能导致银行破产。这事可就大了。还有一种情况,一个在银行里没有什么存款的人,却通过一些技术手段,得到了一个大款的密码甚至是伪装成银行行长,把金库里的钱全提走了,这就是黑客。想想这种感觉你就知道为什么世界上有这么多黑客乐此不疲了。还有一些人没有什么“技术含量”直接“抢银行”,把你的计算机都抱走了。那你只有哭了,金库里的钱丢了,好在“房子”还在呀,这回连“房子”也丢了。依照这个比喻,那木马是什么呢?对,就是你的银行职员里出了内奸了。哈哈。好了不胡扯了,我了这么多例子就是想告诉大家,银行(内核)本身来说还是十分坚固稳定的,问题出在如何通过窗口(系统调用)安全地使用它。这也是学习UNIX系统编程是应该注意的问题。从下一个帖子开始说说说进程(process)和如何生成一个进程。
第二章 进程的生成(1)
先说说什么是进程?假设你编好了一个程序,在它没有被调用之前,它只是乖乖地躺在你的硬盘上,什么事情都不干。好不容易编出来的不干活,这是我们不能容忍的。所以我们要把它调到内存里,然后通过CPU去执行它。所以说,进程就是一个在执行状态下的程序。
我们可以通过
$ps –e
命令来查看一下,计算机里所运行的进程有哪些。
那我们计算机里这么多的进程的又是从哪里来的呢,我们可以通过
$ps –axwf
来看到关于进程的一张家谱。其实,系统中的所有进程都是通过另一个进程生成的(除了0号进程以外),如果,A进程生成了B进程,那么可以把A进程叫做父进程,把B进程叫做A进程的子进程。也就是说,UNIX系统所有的进程都与其它进程保持着父子关系。
下面我们来看一个简单的例子
#include<stdio.h> #include<sys/types.h> #include<unistd.h> int main( int argc , char *argv[]) { int time; time = atoi( argv[1] )*60 ; //将参数中的分钟换成秒数 if( fork()==0 ) //生成子进程,括号里是子进程的代码 { sleep( time ); fprintf( stderr , "it is time to alarm!\n"); } return 0; }
执行时输入
$./a.out 2
这是一个简单的闹钟程序。你把它执行后,看似系统没有什么反映,其实不是,在后台你已经生成了一个进程,来监视时间。如果你用ps命令查看就能看到它。这时你可以接着干你自己的事情。等到了你设定的时间之后这个进程会提示你时间已经到了。这个程序虽不完善(没有进行输入参数的检查),但可以简单的告诉大家如何生成一个进程。
为了生成一个新的进程,这里使用了 fork() 这个系统调用。它的作用是将父进程的各个变量的值复制给子进程,也就是说当你调用了fork()的那一刻,系统就为你生成了一个和父进程完全一样的进程。当然我们不想要一个父亲一样的孩子,孩子要有自己的个性,那我们如何来赋予孩子自己的个性呢?让我们先来看看fork()这个系统调用的概要。
头文件 #include <sys/types.h> #include <unistd.h> 形式 pid_t fork(void); 返回值 成功时: 父进程中:子进程的进程号(>0) 子进程中:=0 失败时: -1
根据上面fork()的特性,我们可以通过fork()的返回值区分父进程要做的事和子进程要做的事。例如,
pid_t pid ; pid=fork(); if( !pid) { //子进程要做的事 }else if(pid >0) { //子进程生成失败时,父进程要做的事 }else //pid<0 { //子进程生成失败时,父进程要做的事 }
好,我们现在已经学会了生成一个子进程了。但它还是遗传了许多父进程的特性,有可能大家会想,能不能用我们生成的子进程来执行另一个与父进程没有任何关系的程序呢。当然是可以的,比如我们常说的shell就是就是这个样子。shell本身也是一个进程当你输入命令回车以后,shell会生成一个子进程来执行你的命令,这条命令可以和shell没有丝毫关系。为了更好的说明问题我们先来做一个简单的shell。当然是最简单的那种。
#include<stdio.h> #include<sys/types.h> #include<unistd.h> int main() { static char prompt[64]="> "; char command[256]; int st; fprintf(stderr,"%s",prompt); // 屏幕上的输出提示符 while(gets(command)!=NULL) // 取得键盘输入 { if(fork()==0) // 生成子进程 { // 子进程要做的事 execl(command,command,(char *)0)==-1 //执行所输入的命令 } else { // 父进程要做的事 wait(&st); // 等待子进程结束 fprintf(stderr,"%s",prompt); // 输出提示符,等待命令 } } return 0; }
好了我们保存,编译,执行以下看看
$./a.out >/bin/ls //这里必须输入命令的完全路径 当前目录下文件名 >Ctrl+D 退出程序 $
这样我们的一个最初级shell就做好了。虽然它还很弱,还有着安全上的漏洞(使用了gets()),甚至连自己退出都不能,但起码可以让我们看到一个shell是如何执行的了。其实,一个复杂的shell最基本的东西也就使这些。大家要是有兴趣的话可以将gets()换掉,再加上退出功能。
我们再说说程序中出现的一个新的函数execl()。其实它是exec函数组中的一个。这组函数有:
int execl( path , arg0 , arg1 , ... , argn , (char *)0 ); int execv( path , argv ); int execle( path , arg0 , arg1 , ... , argn , (char *)0 , envp ); int execve( path , argv , envp ); int execlp( file , arg0 , arg1 , ... , argn , (char *)0 ); int execvp( file , argv );
参数定义如下: char *path; char *file; char *arg0 , *arg1 , ... , *argn; char *argv[]; char *envp[]; 返回值: 成功时:所执行的命令将会覆盖现有的进程,所以无返回值 失败时:-1
比如说我们在shell里执行
$ /bin/ls –l
这个命令,实际上shell调用的是
execl( "/bin/ls" , "/bin/ls" , "-l" , (char *)0 );
的一个系统调用
这个函数组函数有6个,用法就不一一说明了,大家可以参看一下其它资料。这里只告诉大家它们的用处,exec函数组就是用来调用一个可执行程序。还有一点很重要,一但进程调用了exec函数那么写在exec函数后面的进程代码将会被覆盖,变成无效的代码了。
例如下面一段代码,我们想在execl执行后输出一段文字列,这是办不到的。
if(fork()==0) // 生成子进程 { // 子进程要做的事 execl(command,command,(char *)0)==-1 //执行所输入的命令 fprinf(stderr,"lalalalalalalalala!"); // } else { // 父进程要做的事 wait(&st); // 等待子进程结束 fprintf(stderr,"%s",prompt); // 输出提示符,等待命令 }
还有一个函数wait(),它的概要是
#include <sys/types.h> pid_t wait(int *status);
返回值就是子进程的进程号
它的参数是个指针。C语言里讲过,一个函数想有一个以上的返回值时,你可以将想返回的变量的地址作为函数的参数。比如说将数组地址作为函数的参数等等。其实这里的status就是这个道理,它的值与子进程的结束方式有关系。当你的子进程以exit()方式结束的话,status所指向的地址的前8位将会是exit()的参数的后8位,而status所指向的地址的后8位是0。例如子进程是exit(1);那status所指向的地址的内容应该是0000 0001 0000 0000。还有如果子进程是通过信号(signal)终止的(信号我们以后再讲),那么我们也可以通过status的值来判断是哪一个信号终止了这个子进程。(详见man)
我们为什么还要在父进程中调用wait(),这涉及到进程状态的概念,我们稍候再说。