显示标签为“linux”的博文。显示所有博文
显示标签为“linux”的博文。显示所有博文

2008年11月20日星期四

Linux下的C编程实战――驱动程序设计(zz)

1.引言
设备驱动程序是操作系统内核和机器硬件之间的接口,它为应用程序屏蔽硬件的细节,一般来说,Linux的设备驱动程序需要完成如下功能:
(1)初始化设备;
(2)提供各类设备服务;
(3)负责内核和设备之间的数据交换;
(4)检测和处理设备工作过程中出现的错误。
妙不可言的是,Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得Windows的设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。本系列文章的第2章文件系统编程中已经看到了这些函数的真面目,它们就是open ()、close ()、read ()、write () 等。
Linux主要将设备分为二类:字符设备和块设备(当然网络设备及USB等其它设备的驱动编写方法又稍有不同)。这两类设备的不同点在于:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,而块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备主要针对磁盘等慢速设备。以字符设备的驱动较为简单,因此本章主要阐述字符设备的驱动编写。

2.驱动模块函数
init 函数用来完成对所控设备的初始化工作,并调用register_chrdev() 函数注册字符设备。假设有一字符设备“exampledev”,则其init函数为:

void exampledev_init(void)
{
if (register_chrdev(MAJOR_NUM, " exampledev ", &exampledev_fops))
TRACE_TXT("Device exampledev driver registered error");
else
TRACE_TXT("Device exampledev driver registered successfully");
…//设备初始化
}

其中,register_chrdev函数中的参数MAJOR_NUM为主设备号,“exampledev”为设备名,exampledev_fops为包含基本函数入口点的结构体,类型为file_operations。当执行exampledev_init时,它将调用内核函数register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。

file_operations结构体定义为:

struct file_operations
{
int (*lseek)();
int (*read)();
int (*write)();
int (*readdir)();
int (*select)();
int (*ioctl)();
int (*mmap)();
int (*open)();
void(*release)();
int (*fsync)();
int (*fasync)();
int (*check_media_change)();
void(*revalidate)();
};

大多数的驱动程序只是利用了其中的一部分,对于驱动程序中无需提供的功能,只需要把相应位置的值设为NULL。对于字符设备来说,要提供的主要入口有:open ()、release ()、read ()、write ()、ioctl ()。

open()函数 对设备特殊文件进行open()系统调用时,将调用驱动程序的open () 函数:

int open(struct inode * inode ,struct file * file);

其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用MINOR(inode-> i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误) 等;

release()函数 当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release () 函数:

void release (struct inode * inode ,struct file * file) ;

release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。

read()函数 当对设备特殊文件进行read() 系统调用时,将调用驱动程序read() 函数:

void read(struct inode * inode ,struct file * file ,char * buf ,int count) ;

参数buf是指向用户空间缓冲区的指针,由用户进程给出,count 为用户进程要求读取的字节数,也由用户给出。

read() 函数的功能就是从硬设备或内核内存中读取或复制count个字节到buf 指定的缓冲区中。在复制数据时要注意,驱动程序运行在内核中,而buf指定的缓冲区在用户内存区中,是不能直接在内核中访问使用的,因此,必须使用特殊的复制函数来完成复制工作,这些函数在中定义:

void put_user_byte (char data_byte ,char * u_addr) ;
void put_user_word (short data_word ,short * u_addr) ;
void put_user_long(long data_long ,long * u_addr) ;
void memcpy_tofs (void * u_addr ,void * k_addr ,unsigned long cnt) ;

参数u_addr为用户空间地址,k_addr 为内核空间地址,cnt为字节数。

write( ) 函数 当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数:

void write (struct inode * inode ,struct file * file ,char * buf ,int count) ;

write ()的功能是将参数buf 指定的缓冲区中的count 个字节内容复制到硬件或内核内存中,和read() 一样,复制工作也需要由特殊函数来完成:

unsigned char_get_user_byte (char * u_addr) ;
unsigned char_get_user_word (short * u_addr) ;
unsigned char_get_user_long(long * u_addr) ;
unsigned memcpy_fromfs(void * k_addr ,void * u_addr ,unsigned long cnt) ;

ioctl() 函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:

int ioctl (struct inode * inode ,struct file * file ,unsigned int cmd ,unsigned long arg);

参数cmd为设备驱动程序要执行的命令的代码,由用户自定义,参数arg 为相应的命令提供参数,类型可以是整型、指针等。

同样,在驱动程序中,这些函数的定义也必须符合命名规则,按照本文约定,设备“exampledev”的驱动程序的这些函数应分别命名为

exampledev_open、exampledev_ release、exampledev_read、exampledev_write、exampledev_ioctl,因此设备“exampledev”的基本入口点

结构变量exampledev_fops 赋值如下:

struct file_operations exampledev_fops {
 NULL ,
 exampledev_read ,
 exampledev_write ,
 NULL ,
 NULL ,
 exampledev_ioctl ,
 NULL ,
 exampledev_open ,
 exampledev_release ,
 NULL ,
 NULL ,
 NULL ,
 NULL
} ;

3.内存分配

由于Linux驱动程序在内核中运行,因此在设备驱动程序需要申请/释放内存时,不能使用用户级的malloc/free函数,而需由内核级的函数kmalloc/kfree () 来实现,kmalloc()函数的原型为:

void kmalloc (size_t size ,int priority);

参数size为申请分配内存的字节数;参数priority说明若kmalloc()不能马上分配内存时用户进程要采用的动作:GFP_KERNEL 表示等待,即等kmalloc()函数将一些内存安排到交换区来满足你的内存需要,GFP_ATOMIC 表示不等待,如不能立即分配到内存则返回0 值;函数的返回值指向已分配内存的起始地址,出错时,返回0。

kmalloc ()分配的内存需用kfree()函数来释放,kfree ()被定义为:

# define kfree (n) kfree_s( (n) ,0)

其中kfree_s () 函数原型为:

void kfree_s (void * ptr ,int size);

参数ptr为kmalloc()返回的已分配内存的指针,size是要释放内存的字节数,若为0 时,由内核自动确定内存的大小。

4.中断

许多设备涉及到中断操作,因此,在这样的设备的驱动程序中需要对硬件产生的中断请求提供中断服务程序。与注册基本入口点一样,驱动程序也要请求内核将特定的中断请求和中断服务程序联系在一起。在Linux中,用request_irq()函数来实现请求:

int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);

参数irq为要中断请求号,参数handler为指向中断服务程序的指针,参数type 用来确定是正常中断还是快速中断(正常中断指中断服务子程序返回后,内核可以执行调度程序来确定将运行哪一个进程;而快速中断是指中断服务子程序返回后,立即执行被中断程序,正常中断type 取值为0 ,快速中断type 取值为SA_INTERRUPT),参数name是设备驱动程序的名称。

梦想与现实的落差,就是我们离成功的距离~

Linux下的C编程实战――“线程”控制与“线程”通信编程(zz)

1.Linux“线程”
笔者曾经在《基于嵌入式操作系统VxWorks的多任务并发程序设计》(《软件报》2006年第5~12期)中详细叙述了进程和线程的区别,并曾经说明Linux是一种“多进程单线程”的操作系统。Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。大家知道,进程是资源分配的单位,同一进程中的多个线程共享该进程的资源(如作为共享内存的全局变量)。Linux中所谓的“线程”只是在被创建的时候“克隆”(clone)了父进程的资源,因此,clone出来的进程表现为“线程”,这一点一定要弄清楚。因此,Linux“线程”这个概念只有在打冒号的情况下才是最准确的,可惜的是几乎没有书籍留心去强调这一点。

Linux内核只提供了轻量进程的支持,未实现线程模型,但Linux尽最大努力优化了进程的调度开销,这在一定程度上弥补无线程的缺陷。Linux用一个核心进程(轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成。目前Linux中最流行的线程机制为LinuxThreads,所采用的就是线程-进程“一对一”模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。LinuxThreads由Xavier Leroy (Xavier.Leroy@inria.fr)负责开发完成,并已绑定在GLIBC中发行,它实现了一种BiCapitalized面向Linux的Posix 1003.1c “pthread”标准接口。Linuxthread可以支持Intel、Alpha、MIPS等平台上的多处理器系统。按照POSIX 1003.1c 标准编写的程序与Linuxthread 库相链接即可支持Linux平台上的多线程,在程序中需包含头文件pthread. h,在编译链接时使用命令:

gcc -D -REENTRANT -lpthread xxx. c

其中-REENTRANT宏使得相关库函数(如stdio.h、errno.h中函数) 是可重入的、线程安全的(thread-safe),-lpthread则意味着链接库目录下的libpthread.a或libpthread.so文件。使用Linuxthread库需要2.0以上版本的Linux内核及相应版本的C库(libc 5.2.18、libc 5.4.12、libc 6)。

2.“线程”控制

线程创建

进程被创建时,系统会为其创建一个主线程,而要在进程中创建新的线程,则可以调用pthread_create:

pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)(void*), void *arg);

start_routine为新线程的入口函数,arg为传递给start_routine的参数。

每个线程都有自己的线程ID,以便在进程内区分。线程ID在pthread_create调用时回返给创建线程的调用者;一个线程也可以在创建后使用pthread_self()调用获取自己的线程ID:
pthread_self (void) ;

线程退出

线程的退出方式有三:
(1)执行完成后隐式退出;
(2)由线程本身显示调用pthread_exit 函数退出;pthread_exit (void * retval) ;
(3)被其他线程用pthread_cance函数终止:pthread_cance (pthread_t thread) ;
在某线程中调用此函数,可以终止由参数thread 指定的线程。
如果一个线程要等待另一个线程的终止,可以使用pthread_join函数,该函数的作用是调用pthread_join的线程将被挂起直到线程ID为参数

thread的线程终止:

pthread_join (pthread_t thread, void** threadreturn);

3.线程通信

线程互斥

互斥意味着“排它”,即两个线程不能同时进入被互斥保护的代码。Linux下可以通过pthread_mutex_t 定义互斥体机制完成多线程的互斥操作,该机制的作用是对某个需要互斥的部分,在进入时先得到互斥体,如果没有得到互斥体,表明互斥部分被其它线程拥有,此时欲获取互斥体的线程阻塞,直到拥有该互斥体的线程完成互斥部分的操作为止。

下面的代码实现了对共享全局变量x 用互斥体mutex 进行保护的目的:

int x; // 进程中的全局变量
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); //按缺省的属性初始化互斥体变量mutex
pthread_mutex_lock(&mutex); // 给互斥体变量加锁
… //对变量x 的操作
phtread_mutex_unlock(&mutex); // 给互斥体变量解除锁

线程同步
同步就是线程等待某个事件的发生。只有当等待的事件发生线程才继续执行,否则线程挂起并放弃处理器。当多个线程协作时,相互作用的任务必须在一定的条件下同步。

Linux下的C语言编程有多种线程同步机制,最典型的是条件变量(condition variable)。pthread_cond_init用来创建一个条件变量,其函数原型为:

pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *attr);

pthread_cond_wait和pthread_cond_timedwait用来等待条件变量被设置,值得注意的是这两个等待调用需要一个已经上锁的互斥体mutex,这是为了防止在真正进入等待状态之前别的线程有可能设置该条件变量而产生竞争。pthread_cond_wait的函数原型为:
pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);

pthread_cond_broadcast用于设置条件变量,即使得事件发生,这样等待该事件的线程将不再阻塞:
pthread_cond_broadcast (pthread_cond_t *cond) ;
pthread_cond_signal则用于解除某一个等待线程的阻塞状态:
pthread_cond_signal (pthread_cond_t *cond) ;
pthread_cond_destroy 则用于释放一个条件变量的资源。

在头文件semaphore.h 中定义的信号量则完成了互斥体和条件变量的封装,按照多线程程序设计中访问控制机制,控制对资源的同步访问,提供程序设计人员更方便的调用接口。

sem_init(sem_t *sem, int pshared, unsigned int val);

这个函数初始化一个信号量sem 的值为val,参数pshared 是共享属性控制,表明是否在进程间共享。

sem_wait(sem_t *sem);

调用该函数时,若sem为无状态,调用线程阻塞,等待信号量sem值增加(post )成为有信号状态;若sem为有状态,调用线程顺序执行,但信号量的值减一。

sem_post(sem_t *sem);

调用该函数,信号量sem的值增加,可以从无信号状态变为有信号状态。

4.实例

下面我们还是以著名的生产者/消费者问题为例来阐述Linux线程的控制和通信。一组生产者线程与一组消费者线程通过缓冲区发生联系。生产者线程将生产的产品送入缓冲区,消费者线程则从中取出产品。缓冲区有N 个,是一个环形的缓冲池。

#include
#include
#define BUFFER_SIZE 16 // 缓冲区数量

struct prodcons
{
// 缓冲区相关数据结构
int buffer[BUFFER_SIZE]; /* 实际数据存放的数组*/
pthread_mutex_t lock; /* 互斥体lock 用于对缓冲区的互斥操作 */
int readpos, writepos; /* 读写指针*/
pthread_cond_t notempty; /* 缓冲区非空的条件变量 */
pthread_cond_t notfull; /* 缓冲区未满的条件变量 */
};

/* 初始化缓冲区结构 */
void init(struct prodcons *b)
{
pthread_mutex_init(&b->lock, NULL);
pthread_cond_init(&b->notempty, NULL);
pthread_cond_init(&b->notfull, NULL);
b->readpos = 0;
b->writepos = 0;
}

/* 将产品放入缓冲区,这里是存入一个整数*/
void put(struct prodcons *b, int data)
{
pthread_mutex_lock(&b->lock);
/* 等待缓冲区未满*/
if ((b->writepos + 1) % BUFFER_SIZE == b->readpos)
{
pthread_cond_wait(&b->notfull, &b->lock);
}
/* 写数据,并移动指针 */
b->buffer[b->writepos] = data;
b->writepos++;
if (b->writepos > = BUFFER_SIZE)
b->writepos = 0;

/* 设置缓冲区非空的条件变量*/
pthread_cond_signal(&b->notempty);
pthread_mutex_unlock(&b->lock);
}

/* 从缓冲区中取出整数*/
int get(struct prodcons *b)
{
int data;
pthread_mutex_lock(&b->lock);

/* 等待缓冲区非空*/
if (b->writepos == b->readpos)
{
pthread_cond_wait(&b->notempty, &b->lock);
}

/* 读数据,移动读指针*/
data = b->buffer[b->readpos];
b->readpos++;
if (b->readpos > = BUFFER_SIZE)
b->readpos = 0;

/* 设置缓冲区未满的条件变量*/
pthread_cond_signal(&b->notfull);
pthread_mutex_unlock(&b->lock);
return data;
}

/* 测试:生产者线程将1 到10000 的整数送入缓冲区,消费者线程从缓冲区中获取整数,两者都打印信息*/

#define OVER ( - 1)

struct prodcons buffer;

void *producer(void *data)
{
int n;
for (n = 0; n < 10000; n++)
{
printf("%d --->\n", n);
put(&buffer, n);
} put(&buffer, OVER);

return NULL;
}

void *consumer(void *data)
{
int d;
while (1)
{
d = get(&buffer);
if (d == OVER)
break;
printf("--->%d \n", d);
}
return NULL;
}

int main(void)
{
pthread_t th_a, th_b;
void *retval;
init(&buffer);

/* 创建生产者和消费者线程*/
pthread_create(&th_a, NULL, producer, 0);
pthread_create(&th_b, NULL, consumer, 0);
/* 等待两个线程结束*/
pthread_join(th_a, &retval);
pthread_join(th_b, &retval);
return 0;
}

5.WIN32、VxWorks、Linux线程类比

目前为止,笔者已经创作了《基于嵌入式操作系统VxWorks的多任务并发程序设计》(《软件报》2006年5~12期连载)、《深入浅出Win32多线程程序设计》(天极网技术专题)系列,我们来找出这两个系列文章与本文的共通点。
看待技术问题要瞄准其本质,不管是Linux、VxWorks还是WIN32,其涉及到多线程的部分都是那些内容,无非就是线程控制和线程通信,它们的许多函数只是名称不同,其实质含义是等价的,下面我们来列个三大操作系统共同点详细表单:

事项
WIN32
VxWorks
Linux

线程创建
CreateThread
taskSpawn
pthread_create

线程终止
执行完成后退出;线程自身调用ExitThread 函数即终止自己;被其他线程调用函数TerminateThread函数
执行完成后退出;由线程本身调用exit退出;被其他线程调用函数taskDelete终止
执行完成后退出;由线程本身调用pthread_exit 退出;被其他线程调用函数pthread_cance终止

获取线程ID
GetCurrentThreadId
taskIdSelf
pthread_self

创建互斥
CreateMutex
semMCreate
pthread_mutex_init

获取互斥
WaitForSingleObject、

WaitForMultipleObjects
semTake
pthread_mutex_lock

释放互斥
ReleaseMutex
semGive
phtread_mutex_unlock

创建信号量
CreateSemaphore
semBCreate、semCCreate
sem_init

等待信号量
WaitForSingleObject
semTake
sem_wait

释放信号量
ReleaseSemaphore
semGive
sem_post

6.小结
本章讲述了Linux下多线程的控制及线程间通信编程方法,给出了一个生产者/消费者的实例,并将Linux的多线程与WIN32、VxWorks多线程进行了类比,总结了一般规律。鉴于多线程编程已成为开发并发应用程序的主流方法,学好本章的意义也便不言自明。

Linux的进程间通信(zz)

Linux的进程间通信(IPC,InterProcess Communication)通信方法有管道、消息队列、共享内存、信号量、套接口等。管道分为有名管道和无名管道,无名管道只能用于亲属进程之间的通信,而有名管道则可用于无亲属关系的进程之间。

#define INPUT 0
#define OUTPUT 1
void main()
{
int file_descriptors[2];
/*定义子进程号 */
pid_t pid;
char buf[BUFFER_LEN];
int returned_count;

/*创建无名管道*/
pipe(file_descriptors);

/*创建子进程*/
if ((pid = fork()) == - 1)
{
printf("Error in fork\n");
exit(1);
}

/*执行子进程*/
if (pid == 0)
{
printf("in the spawned (child) process...\n");

/*子进程向父进程写数据,关闭管道的读端*/
close(file_descriptors[INPUT]);
write(file_descriptors[OUTPUT], "test data", strlen("test data"));
exit(0);
}
else
{
/*执行父进程*/
printf("in the spawning (parent) process...\n");

/*父进程从管道读取子进程写的数据,关闭管道的写端*/
close(file_descriptors[OUTPUT]);

returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
printf("%d bytes of data received from spawned process: %s\n",
returned_count, buf);
}
}

上述程序中,无名管道以
int pipe(int filedis[2]);
方式定义,参数filedis返回两个文件描述符filedes[0]为读而打开,filedes[1]为写而打开,filedes[1]的输出是filedes[0]的输入;
在Linux系统下,有名管道可由两种方式创建(假设创建一个名为“fifoexample”的有名管道):
(1)mkfifo("fifoexample","rw");
(2)mknod fifoexample p
mkfifo是一个函数,mknod是一个系统调用,即我们可以在shell下输出上述命令。

有名管道创建后,我们可以像读写文件一样读写之:

/* 进程一:读有名管道*/
void main()
{
FILE *in_file;
int count = 1;
char buf[BUFFER_LEN];
in_file = fopen("pipeexample", "r");
if (in_file == NULL)
{
printf("Error in fdopen.\n");
exit(1);
}
while ((count = fread(buf, 1, BUFFER_LEN, in_file)) > 0)
printf("received from pipe: %s\n", buf);
fclose(in_file);
}

/* 进程二:写有名管道*/
void main()
{
FILE *out_file;
int count = 1;
char buf[BUFFER_LEN];
out_file = fopen("pipeexample", "w");
if (out_file == NULL)
{
printf("Error opening pipe.");
exit(1);
}
sprintf(buf, "this is test data for the named pipe example\n");
fwrite(buf, 1, BUFFER_LEN, out_file);
fclose(out_file);
}

消息队列用于运行于同一台机器上的进程间通信,与管道相似;
共享内存通常由一个进程创建,其余进程对这块内存区进行读写。得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式
不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的是实际的物理内存;常用的方式是通过shmXXX函数族来实现共享内存:
int shmget(key_t key, int size, int flag); /* 获得一个共享存储标识符 */
该函数使得系统分配size大小的内存用作共享内存;
void *shmat(int shmid, void *addr, int flag); /* 将共享内存连接到自身地址空间中*/
shmid为shmget函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址。此后,进程可以对此地址进行读写操作访问共享内存。

本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量;
(2)若此信号量的值为正,则允许进行使用该资源,进程将进号量减1;
(3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1);
(4)当进程不再使用一个信号量控制的资源时,信号量值加1,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。

下面是一个使用信号量的例子,该程序创建一个特定的IPC结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后清除信号量:

#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>

void main()
{
key_t unique_key; /* 定义一个IPC关键字*/
int id;
struct sembuf lock_it;
union semun options;
int i;

unique_key = ftok(".", 'a'); /* 生成关键字,字符'a'是一个随机种子*/

/* 创建一个新的信号量集合*/
id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);
printf("semaphore id=%d\n", id);
options.val = 1; /*设置变量值*/
semctl(id, 0, SETVAL, options); /*设置索引0的信号量*/

/*打印出信号量的值*/
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d\n", i);

/*下面重新设置信号量*/
lock_it.sem_num = 0; /*设置哪个信号量*/
lock_it.sem_op = - 1; /*定义操作*/
lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/
if (semop(id, &lock_it, 1) == - 1)
{
printf("can not lock semaphore.\n");
exit(1);
}
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d\n", i);

/*清除信号量*/
semctl(id, 0, IPC_RMID, 0);
}

套接字通信并不为Linux所专有,在所有提供了TCP/IP协议栈的操作系统中几乎都提供了socket,而所有这样操作系统,对套接字的编程方法几乎是完全一样的。

4.小结
本文讲述了Linux进程的概念,并以多个实例讲解了进程控制及进程间通信方法,理解这篇的内容可以说是理解Linux这个操作系统的关键。

2008年9月22日星期一

Linux的shell编程(Linux Shell Tutorial)

http://tech.sina.com.cn 2001/01/15 17:01 软件世界 敬茂华
  Shell本身是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。它虽然不是Linux系统核心的一部分,但它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。因此,对于用户来说,shell是最重要的实用程序,深入了解和熟练掌握shell的特性极其使用方法,是用好Linux系统的关键。可以说,shell使用的熟练程度反映了用户对Linux使用的熟练程度。

  一、什么是shell
  当一个用户登录Linux系统之后,系统初始化程序init就为每一个用户运行一个称为shell(外壳)的程序。那么,shell是什么呢?确切一点说,shell就是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动、挂起、停止甚至是编写一些程序。
  当用户使用Linux时是通过命令来完成所需工作的。一个命令就是用户和shell之间对话的一个基本单位,它是由多个字符组成并以换行结束的字符串。shell解释用户输入的命令,就象DOS里的command.com所做的一样,所不同的是,在DOS中,command.com只有一个,而在Linux下比较流行的shell有好几个,每个shell都各有千秋。一般的Linux系统都将bash作为默认的shell。

  二、几种流行的shell
  目前流行的shell有ash、bash、ksh、csh、zsh等,你可以用下面的命令来查看你自己的shell类型:
  #echo $SHELL
  $SHELL是一个环境变量,它记录用户所使用的shell类型。你可以用命令:
  #shell-name
  来转换到别的shell,这里shell-name是你想要尝试使用的shell的名称,如ash等。这个命令为用户又启动了一个shell,这个shell在最初登录的那个shell之后,称为下级的shell或子shell。使用命令:
  $exit
  可以退出这个子shell。

  使用不同的shell的原因在于它们各自都有自己的特点,下面作一个简单的介绍:
  1.ash
  ash shell是由Kenneth Almquist编写的,Linux中占用系统资源最少的一个小shell,它只包含24个内部命令,因而使用起来很不方便。
  2.bash
  bash是Linux系统默认使用的shell,它由Brian Fox和Chet Ramey共同完成,是Bourne Again Shell的缩写,内部命令一共有40个。Linux使用它作为默认的shell是因为它有诸如以下的特色:
  (1)可以使用类似DOS下面的doskey的功能,用方向键查阅和快速输入并修改命令。
  (2)自动通过查找匹配的方式给出以某字符串开头的命令。
  (3)包含了自身的帮助功能,你只要在提示符下面键入help就可以得到相关的帮助。
  3.ksh
  ksh是Korn shell的缩写,由Eric Gisin编写,共有42条内部命令。该shell最大的优点是几乎和商业发行版的ksh完全兼容,这样就可以在不用花钱购买商业版本的情况下尝试商业版本的性能了。
  4.csh
  csh是Linux比较大的内核,它由以William Joy为代表的共计47位作者编成,共有52个内部命令。该shell其实是指向/bin/tcsh这样的一个shell,也就是说,csh其实就是tcsh。
  5.zch
  zch是Linux最大的shell之一,由Paul Falstad完成,共有84个内部命令。如果只是一般的用途,是没有必要安装这样的shell的。
3. shell程序设计(基础部分)
  其实作为命令语言交互式地解释和执行用户输入的命令只是shell功能的一个方面,shell还可以用来进行程序设计,它提供了定义变量和参数的手段以及丰富的程序控制结构。使用shell编程类似于DOS中的批处理文件,称为shell script,又叫shell程序或shell命令文件。

  1.shell基本语法
  shell的基本语法主要就是如何输入命令运行程序以及如何在程序之间通过shell的一些参数提供便利手段来进行通讯。
  (1)输入输出重定向
  在Linux中,每一个进程都有三个特殊的文件描述指针:标准输入(standard input,文件描述指针为0)、标准输出(standard output,文件描述指针为1)、标准错误输出(standard error,文件描述指针为2)。这三个特殊的文件描述指针使进程在一般情况下接收标准输入终端的输入,同时由标准终端来显示输出,Linux同时也向使用者提供可以使用普通的文件或管道来取代这些标准输入输出设备。在shell中,使用者可以利用“>”和“<”来进行输入输出重定向。如:
  command>file:将命令的输出结果重定向到一个文件。
  command>&file:将命令的标准错误输出一起重定向到一个文件。
  command>>file:将标准输出的结果追加到文件中。
  command>>&file:将标准输出和标准错误输出的结构都追加到文件中。
  command

  (2)管道pipe
  pipe同样可以在标准输入输出和标准错误输出间做代替工作,这样一来,可以将某一个程序的输出送到另一个程序的输入,其语法如下:
  command1| command2[| command3...]
  也可以连同标准错误输出一起送入管道:
  command1| &command2[|& command3...]

  (3)前台和后台
  在shell下面,一个新产生的进程可以通过用命令后面的符号“;”和“&”来分别以前台和后台的方式来执行,语法如下:
  command
  产生一个前台的进程,下一个命令须等该命令运行结束后才能输入。
  command &
  产生一个后台的进程,此进程在后台运行的同时,可以输入其他的命令。

  2。shell程序的变量和参数
  像高级程序设计语言一样,shell也提供说明和使用变量的功能。对shell来讲,所有变量的取值都是一个字符串,shell程序采用$var的形式来引用名为var的变量的值。
  Shell有以下几种基本类型的变量:

  (1)shell定义的环境变量
  shell在开始执行时就已经定义了一些和系统的工作环境有关的变量,这些变量用户还可以重新定义,常用的shell环境变量有:
  HOME:用于保存注册目录的完全路径名。
  PATH:用于保存用冒号分隔的目录路径名,shell将按PATH变量中给出的顺序搜索这些目录,找到的第一个与命令名称一致的可执行文件将被执行。
  TERM:终端的类型。
  UID:当前用户的标识符,取值是由数字构成的字符串。
  PWD:当前工作目录的绝对路径名,该变量的取值随cd命令的使用而变化。
  PS1:主提示符,在特权用户下,缺省的主提示符是“#”,在普通用户下,缺省的主提示符是“$”。
  PS2:在shell接收用户输入命令的过程中,如果用户在输入行的末尾输入“\”然后回车,或者当用户按回车键时shell判断出用户输入的命令没有结束时,显示这个辅助提示符,提示用户继续输入命令的其余部分,缺省的辅助提示符是“>”。

  (2)用户定义的变量
  用户可以按照下面的语法规则定义自己的变量:
  变量名=变量值
  要注意的一点是,在定义变量时,变量名前不应加符号“$”,在引用变量的内容时则应在变量名前加“$”;在给变量赋值时,等号两边一定不能留空格,若变量中本身就包含了空格,则整个字符串都要用双引号括起来。
  在编写shell程序时,为了使变量名和命令名相区别,建议所有的变量名都用大写字母来表示。
  有时我们想要在说明一个变量并对它设定为一个特定值后就不在改变它的值,这可以用下面的命令来保证一个变量的只读性:
  readly 变量名
  在任何时候,建立的变量都只是当前shell的局部变量,所以不能被shell运行的其他命令或shell程序所利用,export命令可以将一局部变量提供给shell执行的其他命令使用,其格式为:
  export 变量名
  也可以在给变量赋值的同时使用export命令:
  export 变量名=变量值
  使用export说明的变量,在shell以后运行的所有命令或程序中都可以访问到。

  (3)位置参数
  位置参数是一种在调用shell程序的命令行中按照各自的位置决定的变量,是在程序名之后输入的参数。位置参数之间用空格分隔,shell取第一个位置参数替换程序文件中的$1,第二个替换$2,依次类推。$0是一个特殊的变量,它的内容是当前这个shell程序的文件名,所以,$0不是一个位置参数,在显示当前所有的位置参数时是不包括$0的。
  (4)预定义变量
  预定义变量和环境变量相类似,也是在shell一开始时就定义了的变量,所不同的是,用户只能根据shell的定义来使用这些变量,而不能重定义它。所有预定义变量都是由$符和另一个符号组成的,常用的shell预定义变量有:
  $#:位置参数的数量
  $*:所有位置参数的内容
  $?:命令执行后返回的状态
  $$:当前进程的进程号
  $!:后台运行的最后一个进程号
  $0:当前执行的进程名
  其中,“$?”用于检查上一个命令执行是否正确(在Linux中,命令退出状态为0表示该命令正确执行,任何非0值表示命令出错)。
  “$$”变量最常见的用途是用作临时文件的名字以保证临时文件不会重复。

  (5)参数置换的变量
  shell提供了参数置换能力以便用户可以根据不同的条件来给变量赋不同的值。参数置换的变量有四种,这些变量通常与某一个位置参数相联系,根据指定的位置参数是否已经设置类决定变量的取值,它们的语法和功能分别如下。
  a. 变量=${参数-word}:如果设置了参数,则用参数的值置换变量的值,否则用word置换。即这种变量的值等于某一个参数的值,如果该参数没有设置,则变量就等于word的值。
  b. 变量=${参数=word}:如果设置了参数,则用参数的值置换变量的值,否则把变量设置成word然后再用word替换参数的值。注意,位置参数不能用于这种方式,因为在shell程序中不能为位置参数赋值。
  c. 变量=${参数?word}:如果设置了参数,则用参数的值置换变量的值,否则就显示word并从shell中退出,如果省略了word,则显示标准信息。这种变量要求一定等于某一个参数的值,如果该参数没有设置,就显示一个信息,然后退出,因此这种方式常用于出错指示。
  d. 变量=${参数+word}:如果设置了参数,则用word置换变量,否则不进行置换。
  所有这四种形式中的“参数”既可以是位置参数,也可以是另一个变量,只是用位置参数的情况比较多。
  接下来以bash为例向大家介绍shell程序设计的高级部分:shell编程的流程控制、调试方法及shell程序的运行方法,顺便也向大家介绍一下bash的内部命令。

  四、shell程序设计的流程控制
  和其他高级程序设计语言一样,shell提供了用来控制程序执行流程的命令,包括条件分支和循环结构,用户可以用这些命令建立非常复杂的程序。
  与传统的语言不同的是,shell用于指定条件值的不是布尔表达式而是命令和字符串。

  1.test测试命令
  test命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试,其测试符和相应的功能分别如下:
  (1)数值测试:
  -eq:等于则为真
  -ne:不等于则为真
  -gt:大于则为真
  -ge:大于等于则为真
  -lt:小于则为真
  -le:小于等于则为真

  (2)字符串测试:
  =:等于则为真
  !=:不相等则为真
  -z字符串:字符串长度伪则为真
  -n字符串:字符串长度不伪则为真

  (3)文件测试:
  -e文件名:如果文件存在则为真
  -r文件名:如果文件存在且可读则为真
  -w文件名:如果文件存在且可写则为真
  -x文件名:如果文件存在且可执行则为真
  -s文件名:如果文件存在且至少有一个字符则为真
  -d文件名:如果文件存在且为目录则为真
  -f文件名:如果文件存在且为普通文件则为真
  -c文件名:如果文件存在且为字符型特殊文件则为真
  -b文件名:如果文件存在且为块特殊文件则为真

  另外,Linux还提供了与(“!”)、或(“-o)、非(“-a”)三个逻辑操作符用于将测试条件连接起来,其优先级为:“!”最高,“-a”次之,“-o”最低。
  同时,bash也能完成简单的算术运算,格式如下:

  $[expression]
  例如:var1=2
  var2=$[var1*10+1]
  则:var2的值为21。

  2.if条件语句
  shell程序中的条件分支是通过if条件语句来实现的,其一般格式为:

  if条件命令串
  then
  条件为真时的命令串
  else
  条件为假时的命令串
  fi

  3.for循环
  for循环对一个变量的可能的值都执行一个命令序列。赋给变量的几个数值既可以在程序内以数值列表的形式提供,也可以在程序以外以位置参数的形式提供。for循环的一般格式为:

  for变量名
  [in数值列表]
  do
  若干个命令行
  done

  变量名可以是用户选择的任何字符串,如果变量名是var,则在in之后给出的数值将顺序替换循环命令列表中的$var。如果省略了in,则变量var的取值将是位置参数。对变量的每一个可能的赋值都将执行do和done之间的命令列表。

  4.while和until循环
  while和until命令都是用命令的返回状态值来控制循环的。While循环的一般格式为:

  while
  若干个命令行1
  do
  若干个命令行2
  done

  只要while的“若干个命令行1”中最后一个命令的返回状态为真,while循环就继续执行do...done之间的“若干个命令行2”。

  until命令是另一种循环结构,它和while命令相似,其格式如下:

  until
  若干个命令行1
  do
  若干个命令行2
  done

  until循环和while循环的区别在于:while循环在条件为真时继续执行循环,而until则是在条件为假时继续执行循环。

  Shell还提供了true和false两条命令用于建立无限循环结构的需要,它们的返回状态分别是总为0或总为非0

  5.case条件选择
  if条件语句用于在两个选项中选定一项,而case条件选择为用户提供了根据字符串或变量的值从多个选项中选择一项的方法,其格式如下:

  case string in
  exp-1)
  若干个命令行1
  ;;
  exp-2)
  若干个命令行2
  ;;
  ……
  *)
  其他命令行
  esac

  shell通过计算字符串string的值,将其结果依次和表达式exp-1、exp-2等进行比较,直到找到一个匹配的表达式为止,如果找到了匹配项则执行它下面的命令直到遇到一对分号(;;)为止。
  在case表达式中也可以使用shell的通配符(“*”、“?”、“[ ]”)。通常用“*”作为case命令的最后表达式以便使在前面找不到任何相应的匹配项时执行“其他命令行”的命令。

  6.无条件控制语句break和continue
  break用于立即终止当前循环的执行,而contiune用于不执行循环中后面的语句而立即开始下一个循环的执行。这两个语句只有放在do和done之间才有效。

  7.函数定义
  在shell中还可以定义函数。函数实际上也是由若干条shell命令组成的,因此它与shell程序形式上是相似的,不同的是它不是一个单独的进程,而是shell程序的一部分。函数定义的基本格式为:

  functionname
  {
  若干命令行
  }
  调用函数的格式为:
  functionname param1 param2……

  shell函数可以完成某些例行的工作,而且还可以有自己的退出状态,因此函数也可以作为if、while等控制结构的条件。
  在函数定义时不用带参数说明,但在调用函数时可以带有参数,此时shell将把这些参数分别赋予相应的位置参数$1、$2、...及$*。

  8.命令分组
  在shell中有两种命令分组的方法:“()”和“{}”,前者当shell执行()中的命令时将再创建一个新的子进程,然后这个子进程去执行圆括弧中的命令。当用户在执行某个命令时不想让命令运行时对状态集合(如位置参数、环境变量、当前工作目录等)的改变影响到下面语句的执行时,就应该把这些命令放在圆括弧中,这样就能保证所有的改变只对子进程产生影响,而父进程不受任何干扰;{}用于将顺序执行的命令的输出结果用于另一个命令的输入(管道方式)。当我们要真正使用圆括弧和花括弧时(如计算表达式的优先级),则需要在其前面加上转义符(\)以便让shell知道它们不是用于命令执行的控制所用。

  9.信号
  trap命令用于在shell程序中捕捉到信号,之后可以有三种反应方式:
  (1)执行一段程序来处理这一信号
  (2)接受信号的默认操作
  (3)忽视这一信号
  trap对上面三种方式提供了三种基本形式:
  第一种形式的trap命令在shell接收到signal list清单中数值相同的信号时,将执行双引号中的命令串。
  trap 'commands' signal-list
  trap "commands" signal-list
  为了恢复信号的默认操作,使用第二种形式的trap命令:
  trap signal-list
  第三种形式的trap命令允许忽视信号:
  trap " " signal-list

  注意:
  (1)对信号11(段违例)不能捕捉,因为shell本身需要捕捉该信号去进行内存的转储。
  (2)在trap中可以定义对信号0的处理(实际上没有这个信号),shell程序在其终止(如执行exit语句)时发出该信号。
  (3)在捕捉到signal-list中指定的信号并执行完相应的命令之后,如果这些命令没有将shell程序终止的话,shell程序将继续执行收到信号时所执行的命令后面的命令,这样将很容易导致shell程序无法终止。

  另外,在trap语句中,单引号和双引号是不同的,当shell程序第一次碰到trap语句时,将把commands中的命令扫描一遍。此时若commands是用单引号括起来的话,那么shell不会对commands中的变量和命令进行替换,否则commands中的变量和命令将用当时具体的值来替换。

  五、运行shell程序的方法
  用户可以用任何编辑程序来编写shell程序。因为shell程序是解释执行的,所以不需要编译装配成目标程序,按照shell编程的惯例,以bash为例,程序的第一行一般为“#!/bin/bash”,其中#表示该行是注释,叹号“!”告诉shell运行叹号之后的命令并用文件的其余部分作为输入,也就是运行/bin/bash并让/bin/bash去执行shell程序的内容。
  执行shell程序的方法有三种:
  (1)sh shell程序文件名
  这种方法的命令格式为:
  bash shell程序文件名
  这实际上是调用一个新的bash命令解释程序,而把shell程序文件名作为参数传递给它。新启动的shell将去读指定的文件,执行文件中列出的命令,当所有的命令都执行完结束。该方法的优点是可以利用shell调试功能。

  (2)sh
  格式为:
  bash
  这种方式就是利用输入重定向,使shell命令解释程序的输入取自指定的程序文件。

  (3)用chmod命令使shell程序成为可执行的
  一个文件能否运行取决于该文件的内容本身可执行且该文件具有执行权。对于shell程序,当用编辑器生成一个文件时,系统赋予的许可权限都是644(rw-r-r--),因此,当用户需要运行这个文件时,只需要直接键入文件名即可。
  在这三种运行shell程序的方法中,最好按下面的方式选择:当刚建立一个shell程序,对它的正确性还没有把握时,应当使用第一种方式进行调试。当一个shell程序已经调试好时,应使用第三种方式把它固定下来,以后只要键入相应的文件名即可,并可被另一个程序所调用。

  六、bash程序的调试
  在编程过程中难免会出错,有的时候,调试程序比编写程序花费的时间还要多,shell程序同样如此。
  shell程序的调试主要是利用bash命令解释程序的选择项。调用bash的形式是:
  bash -选择项shell程序文件名
  几个常用的选择项是:
  -e:如果一个命令失败就立即退出
  -n:读入命令但是不执行它们
  -u:置换时把未设置的变量看作出错
  -v:当读入shell输入行时把它们显示出来
  -x:执行命令时把命令和它们的参数显示出来
  上面的所有选项也可以在shell程序内部用“set -选择项”的形式引用,而“set +选择项”则将禁止该选择项起作用。如果只想对程序的某一部分使用某些选择项时,则可以将该部分用上面两个语句包围起来。

  1.未置变量退出和立即退出
  未置变量退出特性允许用户对所有变量进行检查,如果引用了一个未赋值的变量就终止shell程序的执行。shell通常允许未置变量的使用,在这种情况下,变量的值为空。如果设置了未置变量退出选择项,则一旦使用了未置变量就显示错误信息,并终止程序的运行。未置变量退出选择项为“-u”。
  当shell运行时,若遇到不存在或不可执行的命令、重定向失败或命令非正常结束等情况时,如果未经重新定向,该出错信息会打印在终端屏幕上,而shell程序仍将继续执行。要想在错误发生时迫使shell程序立即结束,可以使用“-e”选项将shell程序的执行立即终止。

  2.shell程序的跟踪
  调试shell程序的主要方法是利用shell命令解释程序的“-v”或“-x”选项来跟踪程序的执行。“-v”选择项使shell在执行程序的过程中,把它读入的每一个命令行都显示出来,而“-x”选择项使shell在执行程序的过程中把它执行的每一个命令在行首用一个“+”加上命令名显示出来。并把每一个变量和该变量所取的值也显示出来,因此,它们的主要区别在于:在执行命令行之前无“-v”则打印出命令行的原始内容,而有“-v”则打印出经过替换后的命令行的内容。
  除了使用shell的“-v”和“-x”选择项以外,还可以在shell程序内部采取一些辅助调试的措施。例如,可以在shell程序的一些关键地方使用echo命令把必要的信息显示出来,它的作用相当于C语言中的printf语句,这样就可以知道程序运行到什么地方及程序目前的状态。

  七、bash的内部命令
  bash命令解释程序包含了一些内部命令。内部命令在目录列表时是看不见的,它们由shell本身提供。常用的内部命令有:echo、eval、exec、export、readonly、read、shift、wait和点(.)。下面简单介绍其命令格式和功能。

  1.echo
  命令格式:echo arg
  功能:在屏幕上打印出由arg指定的字符串。

  2.eval
  命令格式:eval args
  功能:当shell程序执行到eval语句时,shell读入参数args,并将它们组合成一个新的命令,然后执行。

  3.exec
  命令格式:exec命令命令参数
  功能:当shell执行到exec语句时,不会去创建新的子进程,而是转去执行指定的命令,当指定的命令执行完时,该进程,也就是最初的shell就终止了,所以shell程序中exec后面的语句将不再被执行。

  4.export
  命令格式:export变量名或:export变量名=变量值
  功能:shell可以用export把它的变量向下带入子shell从而让子进程继承父进程中的环境变量。但子shell不能用export把它的变量向上带入父shell。
  注意:不带任何变量名的export语句将显示出当前所有的export变量。

  5.readonly
  命令格式:readonly变量名
  功能:将一个用户定义的shell变量标识为不可变的。不带任何参数的readonly命令将显示出所有只读的shell变量。

  6.read
  命令格式:read变量名表
  功能:从标准输入设备读入一行,分解成若干字,赋值给shell程序内部定义的变量。

  7.shift语句
  功能:shift语句按如下方式重新命名所有的位置参数变量:$2成为$1,$3成为$2……在程序中每使用一次shift语句,都使所有的位置参数依次向左移动一个位置,并使位置参数“$#”减一,直到减到0。

  8.wait
  功能:是shell等待在后台启动的所有子进程结束。Wait的返回值总是真。

  9.exit
  功能:退出shell程序。在exit之后可有选择地指定一个数字作为返回状态。

  10.“.”(点)
  命令格式:. Shell程序文件名
  功能:使shell读入指定的shell程序文件并依次执行文件中的所有语句。