Unix环境高级编程笔记——补充

Linux研究 专栏收录该内容
27 篇文章 0 订阅

国外很多牛人出了很多牛书,他们既是科研界的先驱,也是教育界的领军人物,他们的书值得我们反复看反复研究,《Unix环境高级编程》就是其中之一,很久之前就看过一部分,还写了一篇如何运行该书配套程序的博客,最近一段时间在Linux下开发很多,未来很长一段时间可能会继续使用ARM+Linux进行开发。这篇博客时对前一个博客的补充,不过这本书简直就是Linux编程的词典,内容很多,这里重点介绍第一章的内容。

0.Unix和Linux是啥

        如果要写Unix和Linux历史,那足以写一本《三国演义》一样厚的传记。不过天下大事,分久必合合久必分,Unix和Linux也是一样,我们就不再赘述其历史,感兴趣可以自己查。操作系统的作用,我认为就两个,一个是封装硬件,一个是向上层提供各种服务。之前使用过ARM M3的板子,可以直接用裸机程序开发,但是如果要实现一些高级的功能就必须使用回调等比较复杂的方法,而移植了操作系统,很多工作就变得简单。当然,系统也只能移植只有几个文件的FreeRTOS,或者UCosII这种非常小的操作系统,但是个人感觉只要有了操作系统的支持,上层的开发就容易好多。不过难点是如果想在一个低端的设备上移植操作系统,也不是一件容易的工作。

     对于高级设备,操作系统也是一种软件,能够控制计算机的硬件资源,并且为程序提供运行环境。对于Linux这种大型的软件系统,其本身又是分层次的。内核相对较小是系统的核心,提供最基础的系统引导和最基本的硬件驱动加载,稍微高级一点的功能和系统库都由系统调用层来完成。系统调用是承上启下的作用,即扩展了内核的功能,又为应用开发提供了强大的支持。大部分操作系统的差异就体现在系统调用层的差异上。例如windows系统和Linux系统的类库就完全不一样(当然了,都是C/C++代码)。

 Linux还有一种特殊的结构shell,它是一种特殊的应用程序,它可以作为用户程序的接口,甚至可以在用户程序里直接调用shell命令。其实shell就是一个解析命令的工具,类似于早期的DOS命令。windows其实就是将DOS系统的命令换成了大量的图形化界面,而Linux系统由于其应用环境的特殊性,各类命令仍然具有强大的活力。

  在window系统中,盘符的概念深入人心,但是在Linux下,万事万物都是文件,而且是层次化的文件。所有的文件都是根(root,符号为“/”)。根下产生各种各样的目录,例如/home,/opt,/dev等,每以及目录又可以产生子目录,例如/home下又可以有很多的用户名,例如/yangmi,/fuyuanhui,等,而/fuyuanhui下又可以有其他,子子孙孙无穷尽也(当然,目录不要太深)。window是单用户的,一次只能一个用户登录,但是Linux是多用户的,不同的文件和目录可以属于不同的用户、不同的组,也可以有不同的权限。

1.文件和目录

列出某个目录下的文件和目录列表,功能类似命令ls
#include <apue.h>
#include <dirent.h>
#include<my_err.h>

int
main(int argc, char *argv[])
{
	DIR				*dp;
	struct dirent	*dirp;

	if (argc != 2)
		err_quit("usage: ls directory_name");

	if ((dp = opendir(argv[1])) == NULL)
		err_sys("can't open %s", argv[1]);
	while ((dirp = readdir(dp)) != NULL)
		printf("%s\n", dirp->d_name);

	closedir(dp);
	exit(0);
}
然后 gcc 1.1.c生成a.out.
执行./a.out /home ,就可以列出/home下的所有文件或目录名称。
如果想看详细的解释,就请仔细阅读书第5页的内容。

2.输入和输出

每当一个新程序运行时,所有的shell都为其打开三个文件描述符,标准输入、标准输出和标准出错,其实就是输入输出和错误了怎么办。
程序1.2,功能是接收用户输入,并显示或者保存:
#include <apue.h>
#include<unistd.h>
#include<my_err.h>
#define BUFFSIZE 4096
int
main()
{
	int n;
	char buf[BUFFSIZE];


	while((n=read(STDIN_FILENO,buf,BUFFSIZE))>0)
	if(write(STDOUT_FILENO,buf,n)!=n)
		err_sys("write error");
	if(n<0)
		err_sys("read error");
	exit(0);
}
这里比较难理解的是STDIN_FILENO和STDOUT_FILENO,字面意思是标准输入和输出的文件编号。程序运行后能接受用户的输入并直接显示出来。例如
$./a.out 之后程序等待用户输入,每输入一个字符敲回车就马上输出出来。
如果$./a.out >data,那么你输入的文字将保存到data文件中。
如果$./a.out <1.2.c >data,就会将1.2.c的文件复制为文件data.注意,这里的<>不是括号,而是分开的两个符合。"<"是输入,">"是输出。这两个符号是非常常用的。
程序1.3的内容为:
功能与上面类似,不同的是函数open、read、write、lseek和close在进行IO操作是没有缓存,而getc和putc等是有缓存的。
#include <apue.h>
#include<unistd.h>
#include<my_err.h>

int
main()
{
	int c;
	while((c=getc(stdin))!=EOF)
	if(putc(c,stdout)==EOF)
		err_sys("output error");
	if(ferror(stdin))
		err_sys("input error");
	exit(0);

}

1.3程序和进程

程序是放在磁盘上的的文件,它是可以执行的。而进程就是程序执行的执行实例,或者表现形式。说白了,程序是孙悟空的肉身,进程是孙悟空的元神。如果元神出窍,程序就是僵尸,而单纯的谈论进程也是没有意义的。
Linux系统中,每个进程都有一个位置的标识符,一般以数字表示,这个数字类似于数据库里的主键,是保证进程唯一性的。如果要获得进程号,可以使用下面的代码:
#include <apue.h>
 
int
main(void)
{
	 
	printf("hello world from process ID %d\n",getpid());
	exit(0);
}
编译后输入./a.out就可以看到下面的输出:
hello world from process ID 4097.
如果多次执行,进程号一般是不一样的。

如果要控制进程,需要使用多个函数,最主要的是fork创建、exec执行(多种变体,这里是统称)和等待终止waitpid。
例如,一个简单的例子;
#include <apue.h>
#include<sys/wait.h>
#include"my_err.h"
int
main(void)
{
	char buf[MAXLINE];
	pid_t pid;
	int status;
	printf("%%");
 	while(fgets(buf,MAXLINE,stdin)!=NULL)
	{
	if(buf[strlen(buf)-1]=='\n')
	buf[strlen(buf)-1]=0;
	
	if((pid=fork())<0){
		err_sys("fork error");
	}else if(pid==0){
		execlp(buf,buf,(char*)0);
		err_ret("could not execure %s",buf);
	}

	if((pid=waitpid(pid,&status,0))<0)
	err_sys("waitpid error");
	printf("%%");
	}
	 
	exit(0);
}
编译后执行./a.out,可以输出某些命令的执行结果,例如ls、date
    现在来介绍几个重点函数。首先是fork(不是fuck),其功能是创建一个新进程。事实上,很多进程的结构、功能等都是类似的,没有必要每次完全建造新的,新的进程可以复制某个进程来完成。被调用的进程和新进程是父子关系(我感觉母女关系更好,O(∩_∩)O~,有点龌龊啊)。fork在创建新进程时,向父进程返回新子进程的ID号。
  新的进程都是参考已有的进程来创建的,那最早的进程又是什么呢?这是研究Linux内核的时候首先要考虑的问题,在杨立祥老师的书《Linux内核设计的艺术》一书开篇就讲计算机在上电后如何一步步工作的,感兴趣的同学可以看看,以后有时间我们也专门研究一下这本书。
   进程创建之后就要执行某些功能了。在子进程中,调用exec以执行从标准输入读入的命令。exec有多种变体。
       int execl(const char *path, const char *arg, ...);
       int execlp(const char *file, const char *arg, ...);
       int execle(const char *path, const char *arg,
                  ..., 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[]);
      在本书的后面会专门研究这些函数。这些函数的作用是利用一个新的进程镜像(process image)来替代当前的进程镜像。主要说明一下execlp函数,它的第一个参数file用于制定可执行的文件名,第二个参数arg以及后面的可变参都是传递给该可执行文件的参数,由于是可变参数,所以最后一个参数必须以(char*)NULL结尾。该函数如果成功执行,由于是替代当前的进程,所以函数调用语句之后的代码就不会再被执行了。
     这里需要注意的是传递的参数,一般情况下,execlp函数的前两个参数一样,都是可执行文件名。我们考虑带参数的main函数的执行情况,argv[0]指代的实际上是可执行文件名,其后的argv[1]~~argv[n]才是真正传递给它的参数。所以为了保持一致性,由于arg参数以及其后的可变参才是传递给执行程序的参数,所以,arg参数最好传递可执行文件的名字,就是是和file参数保持一致。
     子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这样可以保证所有的进程都在系统的控制之下,这一功能就是waitpid实现的,其参数指定要等待的进程的。waitpid函数返回子进程的终止状态,如果需要可以通过该值判断子进程的终止时间。

  Linux系统中还有一个线程的概念,在编程的时候,其应用甚至比进程更多。一个进程可以对应多个线程。在一个进程内的所有线程共享同一个地址空间、文件描述符、栈空间和其他必要的属性和参数。它们能访问同一个存储区,所以各线程在访问共享数据时会出现不一致的情况,那就需要采取同步措施了。好复杂滴,我们后面再谈。






  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值