第2章 Linux系统开发环境平台
嵌入式Linux应用程序开发与X86计算机上的Linux应用程序开发最主要的区别在于应用平台不一样,相应的编译环境不一样,但由于语法结构、编程理念完全一样,只需要选用正确的编译环境,应用于X86平台上的上层Linux应用程序几乎可以完全不做任何修改就可以移植到嵌入式设备上。因此,本章将详细讲述Linux系统开发的环境平台,主要包括进程/线程管理、文件系统结构和类型、存储管理、设备管理等,这是Linux环境下最基本、最重要的知识点。
2.1 进程/线程管理
进程/线程是Linux对任务管理的基本理论基础,本节主要阐述Linux下进程和线程的基本概念、基本操作及进程间的通信等内容,同时对每一类操作都给出相应的实例。
2.1.1 进程/线程的概念
1.进程的概念
Linux是一个多用户多任务的操作系统,系统的所有任务在内核的调度下在CPU中执行,Linux在很多时候将任务和进程的概念合在一起,进程是一个动态地使用系统资源、处于活动状态的应用程序。Linux进程管理由进程控制块PCB、进程调度、中断管理、任务队列等组成,它是Linux文件系统、存储管理、设备管理和驱动程序的基础。
一个程序可以启动多个进程,它的每个运行副本都有自己的进程空间。
每一个进程都有自己特有的属性,所有这些信息都存储在进程控制块的 PCB 中,主要包括进程PID、进程所占有的内存区域、文件描述符和进程环境等信息,它用task_struct的数据结构表示。此数据结构在Linux的源代码文件夹include/linux/sch.h中定义。
struct task_struct { /* * offsets of these are hardcoded elsewhere - touch with care */ volatile long state; unsigned long flags; /* per process flags, defined below */ int sigpending; mm_segment_t addr_limit; /* thread address space: 0-0xBFFFFFFF for user-thead 0-0xFFFFFFFF for kernel-thread */
struct exec_domain *exec_domain; volatile long need_resched; unsigned long ptrace; int lock_depth; /* Lock depth */ /* * offset 32 begins here on 32-bit platforms. */ unsigned int cpu; int prio, static_prio; struct list_head run_list; prio_array_t *array; unsigned long sleep_avg; unsigned long last_run; unsigned long policy; unsigned long cpus_allowed; unsigned int time_slice, first_time_slice; atomic_t usage; struct list_head tasks; struct list_head ptrace_children; struct list_head ptrace_list; struct mm_struct *mm, *active_mm; /* task state */ struct linux_binfmt *binfmt; int exit_code, exit_signal; int pdeath_signal; unsigned long personality; int did_exec:1; unsigned task_dumpable:1; pid_t pid; pid_t pgrp; pid_t tty_old_pgrp; pid_t session; pid_t tgid; /* boolean value for session group leader */ int leader; struct task_struct *real_parent; struct task_struct *parent; struct list_head children; struct list_head sibling; struct task_struct *group_leader; /* PID/PID hash table linkage. */ struct pid_link pids[PIDTYPE_MAX]; wait_queue_head_t wait_chldexit; /* for wait4() */ struct completion *vfork_done; /* for vfork() */ int *set_child_tid; int *clear_child_tid;
unsigned long rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; struct tms times; struct tms group_times; unsigned long start_time; long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; /* process credentials */ uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; int ngroups; gid_t groups[NGROUPS]; kernel_cap_t cap_effective, cap_inheritable, cap_permitted; int keep_capabilities:1; struct user_struct *user; /* limits */ struct rlimit rlim[RLIM_NLIMITS]; unsigned short used_math; char comm[16]; /* file system info */ int link_count, total_link_count; struct tty_struct *tty; /* NULL if no tty */ unsigned int locks; /* How many file locks are being held */ /* ipc stuff */ struct sem_undo *semundo; struct sem_queue *semsleeping; struct thread_struct thread; struct fs_struct *fs; struct files_struct *files; struct namespace *namespace; struct signal_struct *signal; struct sighand_struct *sighand; sigset_t blocked, real_blocked; struct sigpending pending; unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; /* TUX state */ void *tux_info; void (*tux_exit)(void); /* Thread group tracking */ u32 parent_exec_id; u32 self_exec_id; spinlock_t alloc_lock;
spinlock_t switch_lock; void *journal_info; unsigned long ptrace_message; siginfo_t *last_siginfo; /* For ptrace use. */ }; void (*tux_exit)(void); /* Thread group tracking */ u32 parent_exec_id; u32 self_exec_id; spinlock_t alloc_lock; spinlock_t switch_lock; void *journal_info; unsigned long ptrace_message; siginfo_t *last_siginfo; /* For ptrace use. */ };
2.任务状态及转换
Linux任务(进程)分为以下几种状态,分别是运行状态、等待状态(可以被中断)、等待状态(不可以被中断)、停止状态、睡眠状态和僵死状态。其定义如下:
#define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_STOPPED 4 #define TASK_ZOMBIE 8 #define TASK_DEAD 16
进程的状态转换关系如图2-1所示。
图2-1 进程状态转换图
3.线程的概念
线程是Linux任务管理中另一个重要概念,一个进程中可以包含多个线程,一个线程可以与当前进程中的其他线程进行数据交换,共享数据,但拥有自己的栈空间。线程和进程各有自己的优缺点,线程开销小,但不利于资源的保护,进程相反。线程可以分为内核线程、轻量级线程和用户级线程3种。在Linux应用程序开发中,在很多情况下采用多线程,多线程作为一种多任务、并发的工作方式,有以下的优点。
(1)提高应用程序响应速度。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种情况。
(2)使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
(3)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序有利于理解和修改。
另外,LIBC中的pthread库提供了大量的API函数,为用户编写应用程序提供支持。
2.1.2 进程基本操作
由于进程是任务管理的基本形式,因此,程序员需要有效地对进程进行管理。常见的进程管理方式包括获取进程信息、设置进程属性、创建进程、执行进程、退出进程及跟踪进程6个主要操作。
1.获取进程信息函数
获取进程信息主要通过读取进程控制块PCB中的信息,主要包括:
● getpid()函数获取进程PID;
● getppid()函数获得父进程PID;
● getpgid()函数获得组识别码;
● getpgrp()函数获得当前进程组识别码;
● getpriority()函数获得进程执行的优先级。
(1)getpid()
功能:getpid用来获得目前进程的进程标识。
定义函数:pid_t getpid(void)。
返回值:返回当前进程的进程识别号。
头文件:#include<unistd.h>
(2)getppid()
功能:getppid用来获得目前进程的父进程标识。
定义函数:pid_t getppid(void)。
返回值:返回当前进程的父进程识别号。
头文件:#include<unistd.h>
(3)getpgid()
功能:getpgid用来获得参数pid指令进程所属于的组识别码,如果参数为0,则会返回当前进程的组识别码。
定义函数:pid_t getpgid(pid_t pid)。
返回值:执行成功则返回正确的组识别码,如果有错误则返回-1,错误原因存在于errno中。
头文件:#include<unistd.h>
(4)getpgrp()
功能:getpgrp用来获得目前进程所属于的组识别码。此函数相当于调用getpgid(0)。
定义函数:pid_t getpgrp(void)。
返回值:执行成功则返回正确的组识别码。
头文件:#include<unistd.h>
(5)getpriority获得进程执行的优先级
功能:getpriority用来获得进程、进程组和用户的进程执行优先权。
定义函数:int getpriority(int which,int who)。其中参数如表2-1所示。
表2-1 不同which下who代表的意义
返回值:执行成功则返回当前进程的优先级(-20~20),值越小优先级越高。如果错误返回-1,错误存在于errno中。
头文件:#include<sys/time.h>或<sys/resource.h>
[root@yangzongde ch03_01]# cat get_process_information.c #include<sys/resource.h> //getpriority头文件 #include<unistd.h> main() { printf("the process pid is %d\n",getpid()); //获取进程PID printf("the process parent pid is %d\n",getppid()); //获取父进程PID printf("the current process gid is %d\n",getpgid(getpid())); //获取当前进程的进程组PID printf("the process gid is %d\n",getpgrp()); //获取当前进程的进程组PID printf("the process priority is %d\n",getpriority(PRIO_PROCESS, getpid())); //获取当前进程的优先级 } [root@yangzongde ch03_01]# gcc -o get_process_information get_process_ information.c [root@yangzongde ch03_01]# ls get_process_information get_process_information.c get_process_information.o [root@yangzongde ch03_01]# ./get_process_information the process pid is 13537 the process parent pid is 13438 the current process gid is 13537 the process gid is 13537 the process priority is 0
2.设置进程属性
设置进程属性操作主要用来修改进程PCB中的进程属性,主要包括:
● nice()函数改变进程优先级;
● setpgid()函数设置进程组识别码;
● setpgrp()函数设置进程组识别码;
● setpriority()函数设置程序进程执行优先级。
(1)nice()
功能:nice用来改变进程的进程执行优先级,其参数越大则优先级顺序越低,只有超级用户才能使用负的优先级。
定义函数:int nice(int inc)。
返回值:如果执行成功返回0,否则返回-1,失败原因存在于errno中。
头文件:#include<unistd.h>
(2)setpgid()
功能:setpgid()将参数pid指定进程所属的组识别码设置为参数pgid指定的组识别码,如果参数pid为0,用来设置目前进程的组识别码,如果参数pgid为0,则会以目前进程的进程识别码来取代。
定义函数:int setpgid(pid_t pid,pid_pgid)。
返回值:如果执行成功返回组识别码,否则返回-1,失败原因存在于errno中。
头文件:#include<unistd.h>
(3)setpgrp()
功能:setpgrp用来将目前进程所属于的组识别码设置成为目前进程的进程识别码,此函数相当于调用setpgid(0,0)。
定义函数:int setpgrp(void)。
返回值:如果执行成功返回组识别码,否则返回-1,失败原因存在于errno中。
头文件:#include<unistd.h>
(4)setpriority()
功能:setpriority用来设置进程、进程组和用户的进程优先级,参数which有3种值,应用参数who有3种不同意义。其中参数如表2-2所示。
表2-2 不同which下who代表意义
定义函数:int setpriority(int which,int who,int prio)。
返回值:如果执行成功返回0,否则返回-1,失败原因存在于errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_02]# cat set_process_information.c #include<sys/resource.h> #include<unistd.h> main() { printf("the process priority is %d\n",getpriority(PRIO_PROCESS, getpid()));
nice(10); printf("after nice(10),the process priority is %d\n",getpriority (PRIO_PROCESS,getpid())); printf("the progress gid is %d\n",getpgid(getpid())); printf("the process current priority is %d\n",getpriority(PRIO_ PROCESS,getpid())); setpriority(PRIO_PROCESS,getpid(),-10); printf("the modify process priority is %d\n",getpriority(PRIO_ PROCESS,getpid())); } [root@yangzongde ch03_02]# gcc -o set_process_information set_process_ information.c [root@yangzongde ch03_02]# ./set_process_information the process priority is 0 after nice(10),the process priority is 10 the progress gid is 13759 the process current priority is 10 the modify process priority is -10
3.进程创建
在Linux环境下创建进程主要用来调用fork()函数以建立一个新进程。Linux下所有的进程都是由进程init创建的。fork()函数有以下功能。
功能:fork函数用来产生一个新进程,其子进程会复制父进程的数据和堆栈空间,并继承父进程的用户代码、组代码、环境变量、已经打开的文件代码、工作目录和资源限制。另外,Linux采用写时复制技术(Copy-on-write),只有当进程试图修改欲复制的空间时才会真正地复制数据,由于这些继承的信息是复制而来的,并非指相同的内存空间,因此子进程对这些变量的修改和父进程并不会同步,此外,子进程不会继承父进程的文件锁定和未处理的信号。
定义函数:pid_t fork(void)。
返回值:如果执行成功将在父进程中返回新建子进程的PID,而在新建立的子进程中返回0,如果失败返回-1,失败代码存放在errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_03]# cat fork_example.c #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> extern int errno; int main() { char buf[100]; pid_t cld_pid; int fd; int status; if ((fd=open("temp",O_CREAT|O_TRUNC | O_RDWR,S_IRWXU)) == -1) { printf("open error %d\n",errno); exit(1);
} strcpy(buf,"This is parent process write"); if ((cld_pid=fork()) == 0) { strcpy(buf,"This is child process write"); printf("This is child process"); printf("My PID(child) is %d\n",getpid()); printf("My parent PID is %d\n",getppid()); write(fd,buf,strlen(buf)); close(fd); exit(0); } else { printf("This is parent process"); printf("My PID(parent) is %d\n",getpid()); printf("My child PID is %d\n",cld_pid); write(fd,buf,strlen(buf)); close(fd); } wait(&status); } [root@yangzongde ch03_03]# gcc -o fork_example fork_example.c [root@yangzongde ch03_03]# ./fork_example This is child processMy PID(child) is 13795 My parent PID is 13794 This is parent processMy PID(parent) is 13794 My child PID is 13795 [root@yangzongde ch03_03]#
4.进程执行
用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序替代,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。
有6种不同的exec函数可供使用,具体是execl()、execle()、execlp()、execv()、execve()、execvp(),它们常常被统称为exec函数。这些exec函数都是UNIX进程控制原语。用fork可以创建新进程,用exec可以执行新的程序。exit函数和两个wait函数处理终止和等待终止。这些是需要的基本进程控制原语。在后面各节中将使用这些原语构造另外一些如popen和system之类的函数。
#include<unistd.h> int execl(const char *pathname,const char *arg0,.../*(char*)0*/); int execv(const char *pathname,char *const argv[]); int execle(const char *pathname,const char *arg0, ... /*(char *)0,char *const envp []*/); int execve(const char *pathname,char *const argv[], char *const envp[]); int execlp(const char *filename,const char *arg0,.../*(char *)0*/); int execvp(const char *filename,char *const argv[]);
(1)execl()
功能:execl()用来执行参数path字符串所代表的文件路径(绝对路径),接下来的参数代表执行文件时传递的agrv,最后一个参数必须以空指针结束。
定义函数:int execl(const char *path,const char *arg,...)。
返回值:如果执行成功,不返回,否则失败返回-1,失败代码存于errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_04]# cat execute_example.c #include<unistd.h> main() { execl("/bin/ls","ls","-l","/home",(char *)0); } [root@yangzongde ch03_04]# gcc -o execute_example execute_example.c [root@yangzongde ch03_04]# ./execute_example 总用量9448 drwxr-xr-x 10 root root 4096 6ÔÂ 15 16:58 book drwxr-xr-x 7 2840 562 4096 2002-06-15 Disk1 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk2 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk3 drwxr-xr-x 2 root root 4096 3ÔÂ 27 13:45 document drwx------ 28 ftp1 users 4096 5ÔÂ 13 19:19 ftp1 drwxr-xr-x 4 ftp1 users 4096 3ÔÂ 30 09:28 gui drwx------ 2 root root 16384 3ÔÂ 3 18:46 lost+found drwx------ 13 security_test2 security_test2 4096 3ÔÂ 27 13:46 oracle drwxr-xr-x 4 root root 4096 3ÔÂ 12 10:50 Program drwx------ 3 security_test1 security_test1 4096 3ÔÂ 27 13:54 security _test1 drwx------ 2 security_test2 security_test2 4096 3ÔÂ 26 15:32 security _test2 drwxr-xr-x 2 root root 4096 3ÔÂ 27 10:03 software drwxr-xr-x 4 root root 4096 4ÔÂ 4 15:29 test drwxr-xr-x 5 root root 4096 5ÔÂ 13 19:17 work -rw------- 1 root root 9577556 4ÔÂ 8 17:47 wxX11-2.6.3.tar.gz drwx------ 20 yangzongde yangzongde 4096 4ÔÂ 3 09:58 yangzongde
(2)execle()
功能:execle()用来执行参数path字符串所代表的文件路径(绝对路径),接下来的参数代表执行文件时传递的agrv,最后一个参数必须指向一个新的环境变量数组,此新的环境变量数组即成为新执行程序的环境变量。
定义函数:int execle(const char *path,const char *arg,...,char* const envp[])。
返回值:如果执行成功,不返回,否则失败返回-1,失败原因存于errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_04]# cat execle.c #include<unistd.h> main(int argc,char *argv[],char *env[]) { execle("/bin/ls","ls","-l","/home",(char *)0,env); } [root@yangzongde ch03_04]# gcc -o execle execle.c [root@yangzongde ch03_04]# ./execle ×ÜÓÃÁ¿ 9448 drwxr-xr-x 10 root root 4096 6Ô 15 16:58 book
drwxr-xr-x 7 2840 562 4096 2002-06-15 Disk1 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk2
(3)execlp()
功能:execlp()会从PATH环境变量所指的目录中查找符合参数file的文件名,找到后执行该文件,接下来的参数代表执行文件时传递的agrv[0],最后一个参数必须用空指针NULL。
定义函数:int execlp(const char *path,const char *arg,...)。
返回值:如果执行成功,不返回,否则失败返回-1,失败代码存于errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_04]# cat execlp.c #include<unistd.h> main() { execlp("ls","ls","-l","/home",(char *)0); } [root@yangzongde ch03_04]# gcc -o execlp execlp.c [root@yangzongde ch03_04]# ./execlp 总用量9448 drwxr-xr-x 10 root root 4096 6ÔÂ 15 16:58 book drwxr-xr-x 7 2840 562 4096 2002-06-15 Disk1 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk2 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk3 drwxr-xr-x 2 root root 4096 3ÔÂ 27 13:45 document
(4)execv()
功能:execv()用来执行参数path字符串所代表的文件路径,第二个参数利用数组指针来传递给执行文件。
定义函数:int execv(const char *path, char const *arg[])。
返回值:如果执行成功,不返回,否则失败返回-1,失败代码存于errno中。
库头文件:#include<unistd.h>
[root@yangzongde ch03_04]# cat execv.c #include<unistd.h> main() { char *argv[]={"ls","-l","/home",(char *)0}; execv("/bin/ls",argv); } [root@yangzongde ch03_04]# gcc -o execv execv.c [root@yangzongde ch03_04]# ./execv 总用量9448 drwxr-xr-x 10 root root 4096 6ÔÂ 15 16:58 book drwxr-xr-x 7 2840 562 4096 2002-06-15 Disk1 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk2 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk3 drwxr-xr-x 2 root root 4096 3ÔÂ 27 13:45 document
(5)execve()
功能:execve()用来执行参数filename字符串所代表的文件路径,第二个参数利用数组指针来传递给执行文件,最后一个参数则为传递给执行文件的新环境变量数组。
定义函数:int execve(const char *filename, char *const arg[],char *const envp[])。
返回值:如果执行成功,不返回,否则失败返回-1,失败代码存于errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_04]# cat execve.c #include<unistd.h> main() { char *argv[]={"ls","-l","/home",(char *)0}; char *envp[]={"PATH=/bin",0}; execve("/bin/ls",argv,envp); } [root@yangzongde ch03_04]# gcc -o execve execve.c [root@yangzongde ch03_04]# ./execve total 9448 drwxr-xr-x 7 2840 562 4096 Jun 15 2002 Disk1 drwxrwxr-x 3 2840 562 4096 May 14 2002 Disk2 drwxrwxr-x 3 2840 562 4096 May 14 2002 Disk3 drwxr-xr-x 4 root root 4096 Mar 12 10:50 Program
(6)execvp()
功能:execvp()从PATH环境变量所指的目录中查找符合参数file的文件名,找到后便执行此文件,第二个参数argv传递给要执行的文件。
定义函数:int execvp(const char *filename, char *const arg[])。
返回值:如果执行成功,不返回,否则失败返回-1,失败原因存于errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_04]# cat execvp.c #include<unistd.h> main() { char *argv[]={"ls","-l","/home",0}; execvp("ls",argv); } [root@yangzongde ch03_04]# gcc -o execvp execvp.c [root@yangzongde ch03_04]# ./execvp ×ÜÓÃÁ¿ 9448 drwxr-xr-x 10 root root 4096 6Ô 15 16:58 book drwxr-xr-x 7 2840 562 4096 2002-06-15 Disk1 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk2 drwxrwxr-x 3 2840 562 4096 2002-05-14 Disk3 drwxr-xr-x 2 root root 4096 3Ô 27 13:45 document
5.退出进程
(1)wait()
功能:wait()函数会暂停目前进程的执行,直到有信号来到或子进程结束。如果调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一起返回,如果不需要结束状态值,则参数status可以设置为NULL。
定义函数:pid_t wait(int *status)。
返回值:如果执行成功则返回子进程识别码PID,如果有错误发生则返回-1,失败原因存于errno。
头文件:#include<sys/types.h>,#include<sys/wait.h>
(2)waitpid()
功能:waitpid()函数会暂停目前进程的执行,直到有信号来到或子进程结束,如果调用wait()时子进程已经结束,则waitpid()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一块返回,如果不需要结束状态值,则参数status可以设置为NULL。参数pid为将等待的子进程PID,其数值定义如下。
pid<-1:等待进程组识别码为pid绝对值的任何子进程;
pid=-1:等待任何子进程,相当于wait();
pid=0:等待进程组识别码与目前进程相同的任何子进程;
pid>0:等待任何子进程识别码为pid的子进程。
参数options可以为0或者以下组合。
WHOHANG:如果没有任何已经结束的子进程则马上返回,不予等待;
WUNTRACED:如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。
子进程的结束状态写于status中,以下是常见的几个宏。
WIFEXITED(status):如果子进程正常结束则为非值;
WEXITSTATUS(status):取得子进程由exit()返回的结束代码,一般会先用WIFEXITED判断是否正常结束然后才能作用;
WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真;
WTERMSIG(status):取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED判断后才用此宏。
WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED时才会有此情况。
WSTOPSIG(status):取得引发进程暂停的信号代码,一般会先用 WIFSTOPPED 判断后才使用此宏。
定义函数:pid_t waitpid(pid_t pid,int *status,int options)。
返回值:如果执行成功则返回子进程识别码PID,如果有错误发生则返回-1,失败原因存于errno中。
头文件:#include<sys/types.h>,#include<sys/wait.h>
[root@yangzongde ch03_05]# cat wait_example.c #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> main() { pid_t pid; int status,i; if(fork()==0) { printf("this is the child process pid=%d\n",getpid());
exit(5); }else { sleep(1); printf("this is the parent process wait for child...\n"); pid=wait(&status); i=WEXITSTATUS(status); printf("the child's pid is %d,exit status is %d\n",pid,i); } } [root@yangzongde ch03_05]# gcc -o wait_example wait_example.c [root@yangzongde ch03_05]# ./wait_example this is the child process pid=14914 this is the parent process wait for child... the child's pid is 14914,exit status is 5 [root@yangzongde ch03_05]#
(3)exit()
功能:exit()用来正常结束目前进程的执行,并把参数status返回给父进程,而进程所有的缓冲区数据会自动写回并关闭文件。
定义函数:void exit(status)。
返回值:如果执行成功,不返回,否则失败返回-1,失败原因存于errno中。
头文件:#include<unistd.h>
[root@yangzongde ch03_06]# cat exit_example.c #include<stdlib.h> main() { printf("output begin\n"); printf("content in buffer\n"); exit(0); } [root@yangzongde ch03_06]# gcc -o exit_example exit_example.c [root@yangzongde ch03_06]# ./exit_example output begin content in buffer
(4)_exit()
功能:_exit()用来正常结束目前进程的执行,并把参数status返回给父进程,并关闭文件,此函数调用后不会返回,而是会传递SIGCHLD信号给父进程,父进程可以由wait()函数取得子进程结束状态。
定义函数:void _exit(status)。
返回值:无。_exit()不会处理标准I/O缓冲区,如果要更新需要调用exit()。
头文件:#include<unistd.h>
[root@yangzongde ch03_06]# cat _exit_example.c #include<stdlib.h> main() { printf("output\n"); printf("content in buffer"); _exit(0); }
[root@yangzongde ch03_06]# gcc -o _exit_example _exit_example.c [root@yangzongde ch03_06]# ./_exit_example output [root@yangzongde ch03_06]#
(5)on_exit()
功能:on_exit()用来设置一个程序正常结束前调用的函数,当程序通过调用exit()或者从main中返回时,参数function所指定的函数会被先调用,然后才真正由exit()结束程序,参数arg指针会传给function函数。
定义函数:int on_exit(void(*function)(int void *),void *arg)。
返回值:如果执行成功则返回0,否则返回-1,错误原因存于errno中。
头文件:#include<stdlib.h>
[root@yangzongde ch03_06]# cat on_exit_example.c #include<stdlib.h> void test_exit(int status,void *arg) { printf("before exit()!\n"); printf("exit %d\n",status); printf("arg=%s\n",(char *)arg); } main() { char *str="test"; on_exit(test_exit,(void *)str); exit(4321); } [root@yangzongde ch03_06]# gcc -o on_exit_example on_exit_example.c [root@yangzongde ch03_06]# ./on_exit_example before exit()! exit 4321 arg=test [root@yangzongde ch03_06]#
6.跟踪进程
跟踪进程的实现函数为ptrace()。
功能:ptrace()提供数种服务让父进程来对子进程进行追踪,被追踪子进程处于挂起状态时,父进程可以存取子进程的内存空间,参数request用来要求系统提供的服务有以下几种。
PTRACE_TRACEME:此进程将由父进程追踪;
PTRACE_PEEKTEXT:从pid子进程的内存地址addr中读取一个word;
PTRACE_PEEKDATA:同PTRACE_PEEKTEXT;
PTRACE_PEEKUSR:从pid子进程的USER区域内地址addr中读取一个word;
PTRACE_POKETEXT:将data写入pid子进程的内存地址addr中;
PTRACE_POKEDATA:同PTRACE_POKETEXT;
PTRACE_POKEUSR:将data写入pid子进程的user区域内地址addr中;
PTRACE_SYSCALL:继续pid子进程的执行;
PTRACE_CONT:同PTRACE_SYSCALL;
PTRACE_SINGLESTEP:设置pid子进程的单步追踪旗标;
PTRACE_ATTACH:附带pid子进程;
PTRACE_DETACH:分离pid子进程。
定义函数:int ptrace(int request,int pid ,int addr,int data)。
返回值:如果执行成功则返回0,执行失败则返回-1,失败原因存于errno中。
头文件:#include<sys/ptrace.h>
[root@yangzongde ch03_07]# cat ptrace_example.c #include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #include<signal.h> #include<sys/ptrace.h> #include<asm/ptrace.h> #define attach(pid) ptrace(PRTACE_ATTACH,pid,(char *)1,0) #define detach(pid) ptrace(PTRACE_DETACH,pid,(char *)1,0) #define trace_sys(pid) ptrace(PTRACE_SYSCALL,pid,(char *)1,0) #define trace_me(pid) ptrace(PTRACE_TRACEME,0,(char *)1,0) long get_regs(int pid,int reg) { long val; val=ptrace(PTRACE_PEEKUSER,pid,(char *)(4*reg),0); if(val==-1) perror("ptrace(PTRACE_PEEKUSER,......)"); return val; } void print_regs(pid) { struct pt_regs Regs; Regs.orig_eax=get_regs(pid,ORIG_EAX); Regs.eax=get_regs(pid,EAX); Regs.eip=get_regs(pid,EIP); if(Regs.orig_eax!=0x4) return; printf("ORIG_EAX=0x%x,EAX=0x%x,",Regs.orig_eax,Regs.eax); printf("EIP=0x%x\n",Regs.eip); } int trace_syscall(pid) { int value; value=trace_sys(pid); if(value<0) perror("ptrace"); return value; } main (int argc,char *argv[]) { int pid; int p,status; if(argc<2) { printf("usage:%s program\n",argv[0]);
return; } if(!(pid=fork())) { if(trace_me()<0) perror("ptrace"); execl(argv[1],"traceing",0); }else { printf("ptrace %s(pid=%d)...\n",argv[1],pid); sleep(1); do { trace_syscall(pid); p=wait(&status); if(WIFEXITED(status)) { printf("child exit()\n"); exit(1); } if(WIFSIGNALED(status)) { printf("child exit(),beacuse a signal\n"); exit(1); } print_regs(pid); }while(1); } } [root@yangzongde ch03_07]# gcc -o ptrace_example ptrace_example.c [root@yangzongde ch03_07]# ./ptrace_example /bin/echo ptrace /bin/echo(pid=5418)... ORIG_EAX=0x4,EAX=0xffffffda,EIP=0xffffe002 ORIG_EAX=0x4,EAX=0x1,EIP=0xffffe002 child exit() [root@yangzongde ch03_07]#
2.1.3 进程通信与同步
在一个大型的 Linux应用系统中,各进程间通信显得十分重要。Linux下的进程通信手段绝大多数是从UNIX平台的进程继承而来的,AT&T的贝尔实验室和BSD在进程通信方面各有自己的侧重,Linux将两者继承下来。Linux下进程间通信的几个主要手段如下。
管道:管道有无名管道和有名管道两种,管道只能实现具有亲缘关系的进程间的通信,有名管道克服管道没有名字的限制;
信号:信号为通知进程某一事件发生,从而触发进程执行;
消息队列:消息队列是消息的连接表,有足够权限的进程向消息队列中添加信息,有读权限的进程可以从消息队列中读取消息;
共享内存:共享内存机制使多个进程可以访问同一块内存空间,从而快速地实现进程间的通信;
信号量:信号量主要用于同一进程中各线程之间的信息交互和同步。
1.管道
管道是Linux最先使用的进程通信机制之一,管道只能实现具有亲缘关系的进程间的通信,而有名管道(有名称的管道)克服了这一缺点,管道是单向的,数据只能从一端写入,另一端读取。常见的Linux下管理管道的函数如下。
(1)mkfifo()
功能:mkfifo()会依参数pathname建立特殊的 FIFO 文件,该文件必须存在,而参数mode为该文件的权限(mode%~umask),因此umask值也会影响到 FIFO 文件的权限, mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取。当使用open()函数打开FIFO文件时,O_NONBLOCK会有影响。
● 当使用O_NONBLOCK时,打开FIFO文件来读取的操作会立刻返回,但是如果没有其他进程打开FIFO文件来读取,则写入的操作会返回ENXIO错误代码。
● 没有使用 O_NONBLOCK 旗标时,打开 FIFO 来读取的操作会等到其他进程打开FIFO文件来写入才正常返回。同样地,打开FIFO文件来写入的操作会等到其他进程打开FIFO文件来读取后才正常返回。
定义函数:int mkfifo(const char *pathname,mode_t mode)。
返回值:如果执行成功返回0,否则失败返回-1,失败原因存于errno中。
头文件:#include<sys/types.h>,#include<sys/stat.h>
[root@yangzongde ch03_08]# cat mkfifo_example.c #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<sys/types.h> #include<sys/stat.h> #define FIFO "/tmp/fifo" main() { char buffer[80]; int fd; unlink(FIFO); //delFIFO file mkfifo(FIFO,0744); if(fork()>0) { char s[]="Hello!\n"; fd=open(FIFO,O_WRONLY); write(fd,s,sizeof(s)); close(fd); }else { fd=open(FIFO,O_RDONLY); read(fd,buffer,80); printf("%s",buffer); close(fd); } } [root@yangzongde ch03_08]# gcc -o mkfifo_example mkfifo_example.c [root@yangzongde ch03_08]# ./mkfifo_example [root@yangzongde ch03_08]# Hello! ls
mkfifo_example mkfifo_example.c [root@yangzongde ch03_08]#
(2)pclose()
功能:pclose()用来关闭由popen()所建立的管道及文件指针,参数stream为先前由popen()所返回的文件指针。
定义函数:int pclose(FILE *stream)。
返回值:返回子进程的结束状态,如果有错误则返回-1,错误原因存于errno中。
头文件:#include<unistd.h>
(3)pipe()
功能:pipe()函数建立管道,并将文件描述词由参数filedes数组返回,filedes[0]为管道里的读取端,filedes[1]为管道的写入端。
定义函数:int pipe(int filedes[2])。
返回值:成功返回0,如果有错误则返回-1,错误原因存于errno中。
头文件:#include<unistd.h>
父进程借管道将字符串传递给子进程。
[root@yangzongde ch03_08]# cat pipe_example.c #include<unistd.h> main() { int filedes[2]; char buffer[100]; pipe(filedes); if(fork()>0) {//father char s[]="test"; write(filedes[1],s,sizeof(s)); }else {//son read(filedes[0],buffer,100); printf("the text is %s\n",buffer); } } [root@yangzongde ch03_08]# gcc -o pipe_example pipe_example.c [root@yangzongde ch03_08]# ./pipe_example the text is test
(4)popen()
功能:popen()调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令,参数type可以使用r代表读,w代表写,依靠type的值,popen会建立管道连接到子进程的标准输出设备或标准输入设备或者写入到子进程的标准输入设备中。此外,所有使用文件指针(FILE *)操作的函数也都可以使用,除fclose()外。
定义函数:FILE *popen(const char *comm.and,const char *type)。
返回值:成功返回0,如果有错误则返回-1,错误原因存于errno中。
头文件:#include<stdio.h>
2.信号
信号是模拟中断的一种软件处理机制,一个进程收到一个信号量就相当于收到了一个处理器中断请求,信号是Linux进程通信中的异步通信机制,信号来源有以下两种。
● 硬件来源;
● 软件来源,用发送信号函数实现。这些函数特点如下所示。
(1)kill传送信号给指定进程
功能:kill()可以用来传递参数sig指定的信号给参数pid所指定的进程,参数pid有以下几种情况。
pid>0:将信号传递给进程识别码为pid的进程;
pid=0:将信号传递给目前进程相同的进程组的所有进程;
pid=-1:将信号像广播般传递给系统内所有的进程;
pid<0:将信号传递给进程组识别码为pid绝对值的所有进程。
定义函数:int kill(pid_t pid,int sig)。
返回值:执行成功则返回0,如果有错误则返回-1。
头文件:#include<sys/types.h>,#include<signal.h>
(2)raise()传递信号给目前的进程。
功能:raise()用来将参数sig指定的信号传递给目前的进程,相当于kill(getpid(),sig)。
定义函数:int raise(int sig)。
返回值:执行成功则返回0,否则返回非0值。
库头文件:#include<signal.h>
(3)alarm()
功能:此函数用来设置信号SIGALRM在经过参数seconds指定的时间后传送给目前进程,如果参数seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。
定义函数:unsigned int alarm(unsigned int seconds)。
返回值:返回之前闹钟的剩余时间,如果之前未设定则返回0。
头文件:#include<unsitd.h>
(4)signal()
功能:此函数依参数signum指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。如果参数handler不是函数指针,则必须是以下两个常数之一。
SIG_IGN:忽略参数signum指定的信号;
SIG_DFL:将参数signum指定的信号重设为核心预设的信号处理方式。
定义函数:void(*signal(int signum,void(handler)(int)))(int)。
返回值:返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。
库头文件:#include<signal.h>
(5)singaction()
功能:singaction依参数signum指定的信号编号来设置该信号的处理函数,参数signum可以指定SIGKILL和SIGSTOP以外的所有信号,如果参数act不是NULL指针,则用来设置新的信号处理方式。结构sigaction定义如下:
struct sigaction { void (*sa_handler)(int); sigset_t sa_mask;
int sa_flags; void (*sa_restorer) (void); }
其中:
handler:此参数和signal()的参数handler相同,代表新的信号处理函数;
sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号搁置;
sa_restorer:没有使用;
sa_flags:用来设置信号处理的其他相关操作。
定义函数:int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)。
返回值:执行成功返回0,如果有错误则返回-1。
库头文件:#include<signal.h>
(6)sigprocmask()
功能:此函数用来改变目前的信号遮罩(mask),其操作依参数how来决定。
SIG_BLOCK:新的信号遮罩由目前的信号遮罩和参数set指定的信号遮罩作联集;SIG_UNBLOCK:将目前的信号遮罩删除掉参数set指定的信号遮罩;
SIG_SETMASK:将目前的信号遮罩设置成参数set指定的信号遮罩。
定义函数:int sigprocmask(int how,const sigset_t *set,sigset_t *oldest)。
返回值:执行成功返回0,如果有错误返回-1。
头文件:#include<signal.h>
(7)sigpending()
功能:将被搁置的信号集合由参数set指针返回。
定义函数:int sigpending(sigset_t *set)。
返回值:执行成功返回0,如果有错误返回-1。
库头文件:#include<signal.h>
(8)siguspend()
功能:将目前的信号遮罩暂时换成参数mask所指定的信号遮罩,并将此进程暂停,直到有信号到来才恢复原来的信号遮罩,继续程序执行。
定义函数:int sigsuspend(const sigset_t *mask)。
返回值:返回-1。
头文件:#include<signal.h>
3.消息队列
消息队列是一个消息链接,程序员可以把消息看做一个记录,它具有特定的格式及特定的优先级。对消息队列有写权限的进程可以向消息队列按照一定的规则添加新的消息,而对消息队列有读权限的进程则可以从消息队列中读取信息。目前主要的消息队列有两种类型:POSIX消息队列和系统V消息队列。
每一个消息队列都有一个队列头,队列头中包含了消息队列的绝大多数信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等。
进程可用megsnd( )系统调用来发送一个消息,并将它链入消息队列的尾部。该系统调用的语法格式如下:
int msgsnd(msgid,msgp,msgsz,msgflg) int msgid; struct msgbuf * msgp; int msgsz,msgflg;
其中,msgid是由msgget返回的消息队列描述符;msgp指向包含这条消息的结构,关于消息队列处理主要函数定义如下。
(1)msgget()
功能:msgget()用来取得参数key所关联的信息队列识别代号。如果参数key为 IPC_PRIVATE则会建立新的消息队列,如果KEY不是IPC_PRIVATE,也不是已经建立的IPC key,系统会检查参数msgFlash是否有IPC_CREAT位来决定建立IPC key为key的信息队列。如果参数msgflag包含了IPC_CREAT和IPC_EXCL位,而无法依参数key来建立信息队列,则表示信息队列已经存在。此外,参数msgflag也用来决定信息队列的存取权限,其值相当于open()的参数mode用法。
定义函数:int msgget(key_t key,int msgflg);
返回值:若成功则返回信息队列识别代号,否则返回-1。
头文件:#include<sys/tpye.h>,#include<sys/ipc.h>,#include<sys/msg.h>
(2)msgrcv()
功能:msgrcv()用来从参数msqid指定的信息队列读取信息出来,然后存放到参数msgp所指定的数据结构中,msgbuf的数据结果定义如下:
struct msgbuf { long mtype;//信息种类 char mtext[1];//信息数据 } 参数msgsz为信息数据的长度,即mtext参数的长度;
参数msgtyp是用来指定所要读取的信息种类。msgtyp=0 返回队列内第一项信息;msgtyp>0 返回队列内第一项msgtyp与mtype相同的信息;msgtyp<0 返回队列内第一项mtype小于或等于msgtyp绝对值的信息。
参数msgflg可以设置成 IPC_NOWAIT,意思是如果队列内没有信息可读,则不要等待,立即返回ENOMSG。如果msgflg设置成MSG_NOERROR,则信息大小超过参数msgsz时会被截断。
定义函数:int msgrcv(int msqid,struct msgbuf *msgp,int msgsz,long msgtyp,int msgflg);返回值:若成功则返回实际读取到的信息数据长度,否则返回-1,错误原因存于errno中。
头文件:#include<sys/tpye.h>,#include<sys/ipc.h>,#include<sys/msg.h>
(3)msgsnd()
功能:msgsnd()用来将参数msgp指定的信息送到参数msqid的信息队列中,参数msgp为msgbuf结构。其定义如下:
struct msgbuf { long mtype; //信息种类 char mtext[1]; //信息数据 }
参数msgsz为信息数据的长度,即mtext参数的长度;
参数msgflg可以设置成 IPC_NOWAIT,意思是如果队列已满或者有其他情况无法马上送入信息,则立刻返回EAGIN。
定义函数:int msgsnd(int msqid,stuct msgbuf *msgp,int msgsz,int msgflg);
返回值:若成功则返回0,否则返回-1,错误原因存于errno中。
头文件:#include<sys/tpye.h>,#include<sys/ipc.h>,#include<sys/msg.h>
(4)msgctl()
功能:msgctl()提供了几种方式来控制信息队列的运作,参数msgid为欲处理的信息队列识别代码,参数cmd为欲执行的操作。
定义函数:int msgctl(int semid, int cmd,struct msqid_ds *buf);
返回值:若成功则返回0,否则返回-1,错误原因存于errno中。
头文件:#include<sys/tpye.h>,#include<sys/ipc.h>,#include<sys/msg.h>
4.共享内存
共享内存是最便捷、速率最快的进程通信方式,共享内存方式将同一块物理内存分别映射到A、B两个进程逻辑空间,由于多个进程共享同一块内存空间,因此需要其他同步机制协同工作,如互斥锁和信号量。
(1)shmat()
功能:shmat()用来将参数shmid所指的共享内存和目前进程连接,参数shmid为欲连接的共享内存识别代码,而参数shmaddr有以下几种情况。
0:核心自动选择一个地址;
不为0,但参数shmflg也没有指定SHM_RND旗标:则以参数shmaddr为链接地址;不为0,但参数shmflg设置了SHM_RND旗标:则参数shmaddr会自动调整为SHMLBA的整数位。
定义函数:void *shmat(int shmid,const void *shmaddr,int shmflg);
返回值:若“是”,返回已经连接好的地址,否则返回-1,错误原因存于errno中。
头文件:#include<sys/types.h>,#include<sys/shm.h>
(2)shmctl()
功能:shmctl()提供了几种方式来控制共享内存的操作,参数shmid为欲处理的共享内存识别码,参数cmd为欲控制的操作。其中shmid_ds的结构定义如下:
struct shmid_ds { struct ipc_perm shm_perm; int shm_segsz; //共享内存的大小 time_t shm_atime; //最后一次attach此共享内存的时间 time_t shm_dtime; //最后一次detach此共享内存的时间 time_t shm_ctime; //最后一次更改此共享内存结构的时间 unsigned short shm_cpid; //建立此共享内存的进程识别码 unsigned short shm_lpid; //最后一个操作共享内存的进程识别码 short shm_nattch; unsigned long *shm_pages; struct shm_desc *attaches; }
定义函数:int shmctl(int shmid,int cmd,struct shmid_ds *buf);
返回值:若成功则返回0,否则返回-1,错误原因存于errno中。
头文件:#include<sys/types.h>,#include<sys/shm.h>
(3)shmdt detach共享内存
功能:shmdt()用来将先前用shmat()连接好的共享内存脱离目前进程,参数shmaddr为shmat返回的共享内存地址。
定义函数:int shmdt(const void *shmaddr);
返回值:若成功则返回0,否则返回-1,错误原因存在于errno中。
头文件:#include<sys/types.h>,#include<sys/shm.h>
(4)shmget配置共享内存
功能:shmget()用来取得参数key所关联的共享内存识别代号。
定义函数:int shmget(key_t key,int size,int shmflg);
返回值:若成功,返回共享内存识别码,否则返回-1,错误原因存于errno中。
库头文件:#include<sys/types.h>,#include<sys/shm.h>
5.信号量
(1)semget()
功能:semget()用来取得参数key所关联的信号识别码。如果参数key为IPC_PRIVATE,则会建立新的信号队列,参数nsems为信号集合的数目。如果key不为IPC_PRIVATE,也不是已经建立的信号队列IPC key,那么系统会视参数semflg是否有IPC_CREAT位来决定IPC key为参数key的信号队列。
如果参数semflg包含了IPC_CREAT和IPC_EXCL位,而无法依参数key来建立信号队列,则表示队列已经存在。
定义函数:int semget(key_t key,int nsems,int semflg);
返回值:如果成功则返回信号队列识别码,否则返回-1,错误原因存于errno中。
头文件:#include<sys/types.h>,#include<sys/sem.h>
(2)semctl()
功能:semctl()提供了几种方式来控制信号队列的操作。参数semid为欲处理的信号队列识别码,参数cmd为欲控制的操作。union semun定义如下:
union semun { int val; //SETVAL用的semval值 struct semid_ds *buf; //指向IP_STAT或IPC_set用的semid_ds结构 unsigned short int *array; //GETALL或SETAL用的数组 sturct seminfo *_buf; }
定义函数:int semctl(int semid,int semnum,int cmd,union semun arg);
返回值:若成功则返回0,否则返回-1,错误原因存于errno中。
头文件:#include<sys/types.h>,#include<sys/sem.h>,#include<sys/ipc.h>
(3)semop()
功能:semop()函数中参数semid为欲处理的信号队列识别代码,参数sops指向结构sembuf,其结构定义如下:
struct sembuf { short int sem_num; //欲处理的信号编码,0代表第一个 short int sem_op; short int sem_flg; }
定义函数:int semop(int semid,stuct sembuf *sops,unsigned nsops);
返回值:若成功则返回0,否则返回-1,错误原因存于errno中。
头文件:#include<sys/types.h>,#include<sys/sem.h>,#include<sys/ipc.h>
2.1.4 线程基本操作
1.创建线程
线程创建函数pthread_create用来创建一个新的线程。其函数原型如下所示:
int pthread_create (pthread_t * thread_id, __const pthread_attr_t * __attr, void *(*__start_routine) (void *),void *__restrict __arg)
线程创建函数第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里由于函数thread不需要参数,所以最后一个参数设为空指针。第二个参数也设为空指针,这样将生成默认属性的线程。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
2.等待指定的线程结束
pthread_join函数用来等待一个线程的结束。函数原型为:
int pthread_join (pthread_t __th, void **__thread_return)
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,处于等待状态的线程资源被收回。
3.获得父线程ID
pthread_t pthread_self (void)
4.测试两个线程号是否相同
int pthread_equal (pthread_t __thread1, pthread_t __thread2)
5.线程退出
pthread_exit函数
一个线程的结束有两种途径,一种是函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来结束。它的函数原型为:
void pthread_exit (void *__retval)
该函数的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是 NULL,这个值将被传递给thread_return。需要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
6.线程通信相关函数
使用互斥锁可实现线程间数据的共享和通信,但互斥锁的一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。
(1)pthread_cond_init函数
条件变量的结构为pthread_cond_t,函数pthread_cond_init()用来初始化一个条件变量。它的原型为:
int pthread_cond_init (pthread_cond_t * cond, __const pthread_condattr_t * cond_attr)
其中,cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,与互斥锁一样可以用它来设置条件变量是进程内可用还是进程间可用,默认值是 PTHREAD_PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_destroy(pthread_cond_t cond)。
(2)pthread_cond_wait函数
该函数线程阻塞在一个条件变量上。它的函数原型为:
extern int pthread_cond_wait (pthread_cond_t *__restrict__cond, pthread_ mutex_t *__restrict __mutex)
线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等,这一点从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,等待被下一次唤醒。这个过程一般用while语句实现。
(3)pthread_cond_timedwait函数
另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:
extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,pthread_ mutex_t *__mutex, __const struct timespec *__abstime))
它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
(4)pthread_cond_signal函数
它的函数原型为:
extern int pthread_cond_signal (pthread_cond_t *__cond)
它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。需要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。
(5)互斥量初始化
它的函数原型为:
pthread_mutex_init (pthread_mutex_t *,__const pthread_mutexattr_t *)
(6)销毁互斥量
它的函数原型为:
int pthread_mutex_destroy (pthread_mutex_t *__mutex)
(7)再试一次获得对互斥量的锁定(非阻塞)
它的函数原型为:
int pthread_mutex_trylock (pthread_mutex_t *__mutex)
(8)锁定互斥量(阻塞)
它的函数原型为:
int pthread_mutex_lock (pthread_mutex_t *__mutex)
(9)解锁互斥量
它的函数原型为:
int pthread_mutex_unlock (pthread_mutex_t *__mutex)
2.1.5 简单的多线程编程
本程序为著名的生产者—消费者问题模型的实现,主程序中分别启动生产者线程和消费者线程。生产者线程不断顺序地将0 到1000 的数字写入共享的循环缓冲区,同时消费者线程不断地从共享的循环缓冲区读取数据。流程图如图2-2所示。
图2-2 数据处理流程图
[root@yangzongde ch03_11_pthread]# cat pthread.c #include <stdio.h> #include <stdlib.h> #include <time.h> #include "pthread.h" #define BUFFER_SIZE 16 /* Circular buffer of integers. */ struct prodcons { int buffer[BUFFER_SIZE]; pthread_mutex_t lock; int readpos, writepos; pthread_cond_t notempty; /* signaled when buffer is not empty */ pthread_cond_t notfull; /* signaled when buffer is not full */
}; /*--------------------------------------------------------*/ /* Initialize a buffer */ 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; } /*--------------------------------------------------------*/ /* Store an integer in the buffer */ void put(struct prodcons * b, int data) { pthread_mutex_lock(&b->lock); while ((b->writepos + 1) % BUFFER_SIZE == b->readpos) { printf("wait for not full\n"); 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); sleep(1); } /*--------------------------------------------------------*/ int get(struct prodcons * b) { int data; pthread_mutex_lock(&b->lock); /* Wait until buffer is not empty */ while (b->writepos == b->readpos) { printf("wait for not empty\n"); pthread_cond_wait(&b->notempty, &b->lock); } data = b->buffer[b->readpos]; b->readpos++; if (b->readpos >= BUFFER_SIZE) b->readpos = 0; /* Signal that the buffer is now not full */ pthread_cond_signal(&b->notfull); pthread_mutex_unlock(&b->lock); return data; sleep(1); } /*--------------------------------------------------------*/ #define OVER (-1) struct prodcons buffer; /*--------------------------------------------------------*/ void * producer(void * data) { int n; for (n = 0; n < 1000; n++) { printf(" put-->%d\n", n); put(&buffer, n); } put(&buffer, OVER); printf("producer stopped!\n"); return NULL; } /*--------------------------------------------------------*/ void * consumer(void * data) { int d; while (1) { d = get(&buffer); if (d == OVER ) break; printf(" %d-->get\n", d); } printf("consumer stopped!\n"); 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; }
编译程序
[root@yangzongde ch03_11_pthread]# gcc -o pthread pthread.c -lpthread 运行程序 [root@yangzongde ch03_11_pthread]# ./pthread put-->0 0-->get wait for not empty put-->1 1-->get wait for not empty put-->2 2-->get wait for not empty put-->3 3-->get wait for not empty [root@yangzongde ch03_11_pthread]#
2.2 文件系统结构和类型
本节重点介绍几种常见的文件系统类型,包括FAT文件、RAMFS、JFFS/JFFS2、YAFFS、EXT2/3和/proc。
FAT文件系统是早期Windows平台下使用的,此文件系统简单、易实现,目前在部分嵌入式设备中仍然可以采用。RAMFS 是一个内存文件系统,工作在 Linux的虚拟文件系统VFS层。JFFS/JFFS2是一种用于Nor Flash的文件系统;YAFFS是一种用于NAND Flash的可读可写文件系统;EXT2/3是标准Linux环境下使用的文件系统;/proc是一个内核使用的文件系统,是一个虚拟的文件系统,它通过文件系统接口实现对内核的访问,输出系统的运行状态。
2.2.1 FAT文件系统
每个FAT(File Allocation Table)文件系统由4部分组成,这些基本区域按如下顺序排列分别为:
● 保留区(Reserved Region)。
● FAT区(FAT Region)。
● 根目录区(Root Directory Region,FAT32卷没有此域)。
● 文件和目录数据区(File and Directory Data Region)。
1.保留区
BPB(BIOS Parameter Block)是FAT文件系统中第一个重要的数据结构,它位于该FAT卷的第一个扇区,同时也属于FAT文件系统基本区域的保留区。这个扇区又叫做“启动扇区”、“保留扇区”、“0 扇区”。这是 FAT 文件系统中第一个让人感到迷惑的地方,对于MS-DOS 1.x的版本,启动扇区中并没有BPB。FAT文件系统的最早期版本只有两种不同的格式:用于单面或双面的360KB的5寸软盘。这两种格式是通过FAT的第一个字节(FAT[0]的低8位)来区分的。
在MS-DOS 2.x以后,启动扇区里增加了BPB用于区分磁盘介质,同时不再支持老的磁盘介质区分方式(用FAT的第一个字节来区分),所有的FAT文件系统卷必须在启动扇区中包含BPB。这又是一个迷惑人的地方,BPB究竟是什么样的?在MS-DOS 2.x的定义中,每个FAT卷的扇区数不能多于65536(每个扇区512字节的话最多32MB),这一限定是由于定义“总扇区数”的变量本身是一个16-bit的数据类型。这一限制在MS-DOS 3.x中有所改进,它使用一个32-bit的变量来存储“总扇区数”。
在Windows 95操作系统,确切地说应该是在OSR2(OEM Service Release 2)出现的时候,BPB的内容有了新的变化,在这一版本中引入了新的FAT类型——FAT32。在FAT16中,由于 FAT 表的大小限制了有效的簇数(Cluster),同时也就限制了磁盘空间的大小,如果每个扇区为512字节的话,那么FAT16格式只能支持到2GB。FAT32的引入改变了这一状况,不再需要增加分区来管理大于2GB的硬盘。
FAT32的BPB内容与FAT12/FAT16的内容在BPB_ToSet32区域以前完全一致,而从偏移量36开始,它们的内容有所区别,具体内容要看FAT类型为FAT12/FAT16还是FAT32 (后面的内容会提到如何区分 FAT 格式),这点保证了在启动扇区中包含一个完整的FAT12/FAT16或FAT32的BPB内容,这么做是为了达到最好的兼容性,同时也为了保证所有的 FAT 文件系统驱动程序能正确地识别和驱动不同的 FAT 格式,并让它们良好地工作,因为它们包含了现有的全部内容。表2-3列出保留区的相关信息。
表2-3 保留区各字节信息
2.FAT数据结构(FAT Data Structure)
接下来一个重要的数据结构就是FAT表(File Allocation Table),它是一 一对应于数据区簇号的列表。
由于文件系统分配磁盘空间按簇来分配的。因此,文件占用磁盘空间时,基本单位不是字节而是簇,即使某个文件只有一个字节,操作系统也会给它分配一个最小单元——一个簇。为了可以将磁盘空间有序地分配给相应的文件,而读取文件的时候又可以从相应的地址读出文件,将数据区空间分成BPB_BytsPerSec*BPB_SecPerClus字节长的簇来管理,FAT表项的大小与FAT类型有关,FAT12的表项为12-bit,FAT16为16-bit,而FAT32则为32-bit。对于大文件,需要分配多个簇。同一个文件的数据并不一定完整地存放在磁盘中一个连续的区域内,而往往会分成若干段,像链表一样存放。这种存储方式称为文件的链式存储。为了实现文件的链式存储,文件系统必须准确地记录哪些簇已经被文件占用,还必须为每个已经占用的簇指明存储后继内容的下一个簇的簇号,对文件的最后一簇,则要指明本簇无后继簇。
以上这些都是由FAT表来保存的,FAT表的对应表项中记录着它所代表的簇的有关信息:诸如是否空,是否是坏簇,是否已经是某个文件的尾簇等。
FAT的项数与硬盘上的总簇数相关(因为每一个项要代表一个簇,簇越多当然需要的FAT 表项越多),每一项占用的字节数也与总簇数有关(因为其中需要存放簇号,簇号越大当然每项占用的字节数就大)。这里介绍一下FAT目录,其实它和普通的文件并没有什么不一样的地方,只是多了一个表示它是目录的属性(attrib),另外就是目录所链接的内容是一个32字节的目录项(32-byte FAT directory entries后面有具体讨论)。除此之外,目录和文件没什么区别。FAT表是根据簇数和文件对应的。第一个存放数据的簇是簇2。
簇2的第一个扇区(磁盘的数据区)根据BPB来计算,首先计算根目录所占的扇区数:RootDirSectors = ((BPB_RootEntCnt * 32) + (BPB_BytsPerSec - 1)) / BPB_BytsPerSec;
因为FAT32的BPB_RootEntCnt为0,所以对于FAT32卷RootDirSectors的值也一定是0。上式中的32是每个目录项所占的字节数。计算结果四舍五入。
数据区的起始地址,簇2的第一个扇区由下面公式计算:
If(BPB_FATSz16 != 0) FATSz = BPB_FATSz16; Else FATSz = BPB_FATSz32; FirstDataSector = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) + RootDirSectors;
注意:扇区号指的是针对卷中包含BPB的第一个扇区的偏移量(包含BPB的第一个扇区是扇区0),并不是必须直接和磁盘的扇区相对应。因为卷的扇区0并不一定就是磁盘的扇区0。给一个合法的簇号N,该簇的第一个扇区号(针对FAT卷扇区0的偏移量)由下式计算:
FirstSectorofCluster = ((N - 2) * BPB_SecPerClust) + FirstDataSector;
注意:因为BPB_SecPerClus总是2的整数次方(1,2,4,8,…)这意味着BPB_SecPerClus的乘除法运算可以通过移位(SHIFT)来进行。在当前Intel x86架构二进制的机器上乘法(MULT)和除法(DIV)的机器指令非常繁杂和庞大,而使用移位来运算则会相对快许多。
3.FAT目录结构(FAT Directory Structure)
FAT目录其实就是一个由32字节的线性表构成的“文件”。根目录(root directory)是一个特殊的目录,它存在于每一个 FAT 卷中。对于 FAT12/16,根目录存储在磁盘中固定的地方,它紧跟在最后一个 FAT 表后。根目录的扇区数也是固定的,可以根据 BPB_RootEntCnt计算得出(参见前文计算公式),对于FAT12/16,根目录的扇区号是相对该FAT卷第一个扇区(0扇区)的偏移量。
FirstRootDirSecNum = BPB_RsvdSecCnt + (BPB_NumFATs * BPB_FATSz16);
FAT32的根目录由簇链组成,其扇区数不确定,这点与普通的文件相同,根目录的第一个扇区号存储在 BPB_RootClus中,根目录不同于其他的目录,没有日期和时间戳,也没有目录名(“/”并不是其目录名),同时根目录里没有“.”和“..”这两个目录项,根目录另一个特殊的地方在于,根目录中有一个设置了ATTR_VOLUME_ID位(如表2-4所示)的文件,这个文件在整个FAT卷中是唯一的。表2-4列出了32字节目录项结构。
表2-4 FAT的32字节目录项结构
2.2.2 RAMFS内核文件系统
RAMFS是一个利用VFS自身结构的内存文件系统,RAMFS没有自己的文件存储结构,它的文件存储于page cache中,目录结构由dentry链表本身描述,文件由VFS的inode结构描述。在Linux2.4.20内核中,fs/ramfs/inode.c定义这一文件系统和文件系统的基本操作。
(1)page操作:对页的操作。
static struct address_space_operations ramfs_aops = { readpage: ramfs_readpage, writepage: fail_writepage, prepare_write: ramfs_prepare_write, commit_write: ramfs_commit_write };
(2)文件操作。
static struct file_operations ramfs_file_operations = { read: generic_file_read, write: generic_file_write, mmap: generic_file_mmap, fsync: ramfs_sync_file, };
(3)inode操作。
static struct inode_operations ramfs_dir_inode_operations = { create: ramfs_create, lookup: ramfs_lookup, link: ramfs_link, unlink: ramfs_unlink, symlink: ramfs_symlink, mkdir: ramfs_mkdir, rmdir: ramfs_rmdir, mknod: ramfs_mknod, rename: ramfs_rename, };
(4)超级操作。
static struct super_operations ramfs_ops = { statfs: ramfs_statfs, put_inode: force_delete, };
ramfs文件系统模板在Linux下以模块方式实现,以下是实现代码。
static int __init init_ramfs_fs(void) { return register_filesystem(&ramfs_fs_type); } static void __exit exit_ramfs_fs(void) { unregister_filesystem(&ramfs_fs_type); } module_init(init_ramfs_fs) module_exit(exit_ramfs_fs) int __init init_rootfs(void) { return register_filesystem(&rootfs_fs_type); }
2.2.3 JFFS与YAFFS文件系统
JFFS文件系统是瑞典Axis Communications AB为嵌入式系统开发的日志文件系统。JFFS1应用在Linux 2.2以上版本中,JFFS2在Linux 2.4内核和Ecos中。在Linux的实现中,JFFS必须建立在MTD驱动程序的上层。
1.JFFS文件系统
由于 JFFS 是针对以闪存为存储介质的嵌入式系统,所以充分考虑了闪存的物理局限性,使用了尽可能高效的日志系统,使文件系统更加可靠。与前面介绍的TrueFFS及其他中间层驱动相比,JFFS是专门针对闪存的文件系统,这个文件系统除了有日志功能外,还包含了前面在TrueFFS章节中介绍的负载平衡、垃圾收集等功能。另外一个重要特点是这个文件系统是源代码公开的,方便了学习和使用。
日志文件系统的主要设计思想是跟踪文件系统的变化而不是文件系统的内容。在日志文件系统中,存储系统上面有一系列节点记录了对文件的操作。日志节点上面记录的信息包括以下内容。
● 与日志节点关联的文件的标示符。
● 日志节点的序列号(version)。
● 当前节点的uid、gid等信息。
● 其他关于文件内容分布的信息。
JFFS是一种纯日志文件系统。Linux中所谓文件系统是一系列存放在存储介质上的节点。在JFFS1中,只有一种日志节点struct jffs_raw_inode用于在闪存芯片中存放数据,节点的定义如下:
struct jffs_raw_inode { __u32 magic; /* 魔数 */ __u32 ino; /* I 节点编号 */ __u32 pino; /* 该节点的父节点编号 */ __u32 version; /* Version 号 */ __u32 mode; /* 文件类型 */ __u16 uid; /* 属组 */ __u16 gid; /* 文件属组 */ __u32 atime; /* 最后访问时间 */ __u32 mtime; /*最后修改时间 */ __u32 ctime; /* 创建时间 */ __u32 offset; /* 数据偏移 */ __u32 dsize; /* 数据长度 */ __u32 rsize; /* 要删除的数据长度 */ __u8 nsize; /* 名称长度 */ __u8 nlink; /* 文件链接数 */ __u8 spare : 6; /* 保留位 */ __u8 rename : 1; /* 是否需要更名? */ __u8 deleted : 1; /* 是否被删除 */ __u8 accurate; /* 是否是可用的数据 */ __u32 dchksum; /* 数据校验码 */ __u16 nchksum; /* 名称校验码 */ __u16 chksum; /* 节点信息校验码 */ };
2.JFFS2文件系统
JFFS2是Redhat公司基于JFFS开发的闪存文件系统,它主要是针对Redhat公司的嵌入式产品eCos开发的嵌入式文件系统,当然JFFS2也可以使用在Linux、ucLinux中。JFFS2克服了JFFS中的如下缺点:
● 使用了基于哈希表的日志节点结构,大大加快了对节点的操作速度。
● 支持数据压缩。
● 提供了“写平衡”支持。
● 支持多种节点类型(数据I节点,目录I节点等);而JFFS只支持一种节点。
● 提高了对闪存的利用率,降低了内存的消耗。
JFFS2中定义了多种节点,但是每种节点都包含下面的信息。
struct jffs2_unknown_node { __u16 magic; /*作为nodetype 的补充*/ __u16 nodetype; /*节点类型*/ __u32 totlen; /* 节点总长度 */ __u32 hdr_crc;/*CRC 校验码*/ };
3.YAFFS文件系统
YAFFS(Yet Another Flash File System)类似于JFFS/JFFS2,是专门为NAND闪存设计的嵌入式文件系统,适用于大容量的存储设备。它是日志结构的文件系统,提供了损耗平衡和掉电保护,可以有效地避免意外掉电对文件系统一致性和完整性的影响。YAFFS文件系统是按层次结构设计的,分为文件系统管理层接口、YAFFS内部实现层和NAND接口层,这样就简化了其与系统的接口设计,可以方便地集成到系统中去。与 JFFS 相比,它减少了一些功能,因此速度更快,占用内存更少。
YAFFS充分考虑了NAND闪存的特点,根据NAND闪存以页面为单位存取的特点,将文件组织成固定大小的数据段。利用NAND闪存提供的每个页面16字节的备用空间来存放ECC(Error Correction Code)和文件系统的组织信息,不仅能够实现错误检测和坏块处理,也能够提高文件系统的加载速度。YAFFS采用一种多策略混合的垃圾回收算法,结合了贪心策略的高效性和随机选择的平均性,达到了兼顾损耗平均和系统开销的目的。
(1)YAFFS文件组织结构:YAFFS将文件组织成固定大小(512字节)的数据段。每个文件都有一个页面专门存放文件头,文件头保存了文件的模式、所有者id、组id、长度、文件名等信息。为了提高文件数据块的查找速度,文件的数据段被组织成树形结构。YAFFS在文件进行改写时总是先写入新的数据块,然后将旧的数据块从文件中删除。YAFFS使用存放在页面备用空间中的 ECC 进行错误检测,出现错误后会进行一定次数的重试,多次重试失败后,该页面就被停止使用。
(2)YAFFS物理数据组织:YAFFS充分利用了NAND闪存提供的每个页面16字节的备用空间,参考了SmartMedia的设定,备用空间中6个字节被用做页面数据的ECC,两个字节分别用做块状态字和数据状态字,其余的8字节(64位)用来存放文件系统的组织信息,即元数据。由于文件系统的基本组织信息保存在页面的备份空间中,因此,在文件系统加载时只需要扫描各个页面的备份空间,即可建立起整个文件系统的结构,而不需要像JFFS那样扫描整个介质,从而大大加快了文件系统的加载速度。
(3)YAFFS 擦除块和页面分配:YAFFS 中用数据结构来描述每个擦除块的状态。该数据结构记录了块状态,并用一个32位的位图表示块内各个页面的使用情况。在YAFFS中,有且仅有一个块处于“当前分配”状态。新页面从当前进行分配的块中顺序进行分配,若当前块已满,则顺序寻找下一个空闲块。
(4)YAFFS 垃圾收集机制:YAFFS 使用一种多策略混合的算法来进行垃圾回收,将贪心策略和随机选择策略按一定比例混合使用,当满足特定的小概率条件时,垃圾回收器会试图随机选择一个可回收的页面。通过使用多策略混合的方法,YAFFS能够有效地改善贪心策略造成的不平均;通过不同的混合比例,则可以控制损耗平均和系统开销之间的平衡。考虑到NAND的擦除很快(与NOR相比可忽略不计),YAFFS将垃圾收集的检查放在写入新页面时进行,而不是采用JFFS那样的后台线程方式,从而简化了设计。
在Linux 2.6内核中加入了这一文件系统,在fs/yaffs/yaffs.c中详细定义此文件系统相关数据结构和操作。
文件操作。
static struct file_operations yaffs_file_operations = { #ifdef CONFIG_YAFFS_USE_GENERIC_RW read: generic_file_read, write: generic_file_write, #else read: yaffs_file_read, write: yaffs_file_write, #endif mmap: generic_file_mmap, flush: yaffs_file_flush, fsync: yaffs_sync_object, };
inode操作。
static struct inode_operations yaffs_file_inode_operations = { setattr: yaffs_setattr, };
dir_inode操作。
static struct inode_operations yaffs_dir_inode_operations = { create: yaffs_create, lookup: yaffs_lookup, link: yaffs_link, unlink: yaffs_unlink, symlink: yaffs_symlink, mkdir: yaffs_mkdir, rmdir: yaffs_unlink, mknod: yaffs_mknod, rename: yaffs_rename, setattr: yaffs_setattr, };
dir操作。
static struct file_operations yaffs_dir_operations = { read: generic_read_dir,
readdir: yaffs_readdir, fsync: yaffs_sync_object, };
yaff_super操作。
static struct super_operations yaffs_super_ops = { statfs: yaffs_statfs, read_inode: yaffs_read_inode, put_inode: yaffs_put_inode, put_super: yaffs_put_super, // remount_fs: delete_inode: yaffs_delete_inode, clear_inode: yaffs_clear_inode, };
2.2.4 EXT2/EXT3文件系统
EXT2(The Second Extended File System)是由Remy Card发明的,它是Linux的一个可扩展的、强大的文件系统。至少在Linux社区中,EXT2 是最成功的文件系统,是所有当前的Linux发布版的基础。
与大多数文件系统一样,EXT2 文件系统建立在这样的前提下:文件的数据存放在数据块中,这些数据块的长度都相同。虽然不同的EXT2文件系统的块长度可以不同,但是对于一个特定的EXT2文件系统,在它创建时,其块长度就确定了(使用mke2fs)。每一个文件的长度都按块取整。如果块大小是1024字节,一个1025字节的文件会占用两个1024字节的块。这意味着平均每一个文件要浪费半个块。在通常的计算中,用户会用内存和磁盘的使用来交换对CPU的使用(空间交换时间),在这种情况下,Linux如大多数操作系统一样,会为了较少的 CPU 负载,而使用相对低效的磁盘利用率。不是文件系统中所有的块都用来存储数据,因为必须用一些块放置描述文件系统结构的信息。
EXT2 用一个inode数据结构描述系统中的每一个文件,从而定义了文件系统的拓扑结构。一个inode描述了一个文件中的数据占用了哪些块及文件的访问权限、文件的修改时间和文件的类型等。EXT2文件系统中的每一个文件都用一个inode描述,而每一个inode都用一个独一无二的数字标识。文件系统的所有inode都放在inode表中。EXT2的目录是简单的特殊文件(它们也使用inode描述),该文件的内容是一组指针,每一个指针指向一个inode,该inode描述了目录中的一个文件。
如图2-3所示是一个EXT2文件系统的布局,该文件系统占用了一个块结构设备上的一系列的块。从文件系统所关心的角度来看,块设备都可以被当做一系列能够读写的块。文件系统自己不需要关心一个块应该放在物理介质的哪个位置,因为这是设备驱动程序的工作。当一个文件系统需要从包括它的块设备上读取信息或数据的时候,它只是请求支撑它的设备驱动程序来读取整数数目的块。
图2-3 EXT2文件系统的布局
EXT2 文件系统把它占用的逻辑分区划分成块组(Block Group)。每一个组除了当做信息和数据的块来存放真实的文件和目录之外,还复制对于文件系统一致性至关重要的信息。当发生灾难、文件系统需要恢复的时候,这些复制的信息是必要的。下面对于每一个块组的内容进行了详细的描述。
1.The EXT2 Inode
在 EXT2 文件系统中,I 节点是建设的基石,文件系统中的每一个文件和目录都用一个且只用一个inode描述。每一个块组的EXT2inode都集中放在inode表中,另有一个位图,让系统跟踪inode表中分配和未分配的I节点信息。如图2-4所示为一个EXT2 inode的格式,它包括下面一些域。
图2-4 Inode节点
(1)Mode:包括两组信息,这个inode描述的是什么和使用它的用户应具有的权限。对于EXT2,一个inode可以描述一个文件、目录、符号链接、块设备、字符设备或FIFO。
(2)Owner Information:这个文件或目录的属主的用户和组标识符。这允许文件系统正确地进行文件访问权限控制(分类)。
(3)Size:文件的大小(字节)。
(4)Timestamps:这个inode创建的时间和它上次被修改的时间。
(5)Datablocks:指向这个inode描述的数据所占用的一组块的指针。前面的12个是指向这个inode描述的数据的物理块的指针,最后的3个指针包括更多级的间接的数据块。例如,两级的间接块指针指向一个数据块,该数据块中包含指向物理块的指针。这意味着访问小于或等于12 个数据块的文件要比访问大于12 个数据块的文件快。
2.EXT2目录
EXT2 文件系统中,目录是特殊文件,用来创建和存放到文件系统中的文件的访问路径。如图2-5所示为内存中一个目录条目的布局。一个目录文件是一个目录条目的列表,每一个目录条目包括以下信息。
图2-5 EXT2目录
(1)inode:目录的inode。这是一个索引,是该inode在块组的inode表中的索引。在图2-5中,文件名为“file”的文件的目录条目所引用的inode为“i1”。
(2)Name length:这个目录条目的长度(以字节计)。
(3)Name:这个目录条目的名字。每一个目录中的前两个条目总是标准的“.”和“..”,分别表示“本目录”和“父目录”。
3.在EXT2文件系统中查找文件
Linux的文件名和UNIX文件名的格式一样。一个文件名的例子是/home/rusling/.cshrc,其中/home和/rusling是路径,文件名是.cshrc。像其他UNIX系统一样,Linux并不关心文件名本身的格式,它是由可打印字符组成的任意长度的字符串。为了在EXT2文件系统中找到代表这个文件的inode,系统必须一个目录一个目录地解析文件名,直到得到这个文件本身。
需要的第一个inode是这个文件系统的根的inode。可以通过文件系统的超级块找到它的编号。为了读取一个EXT2 inode,必须在适当的块组中的inode表中查找。例如,如果根的inode编号是42,那么需要块组0中的inode表中的第42个inode。Root inode是一个EXT2目录,换句话说root inode的模式(mode域)描述它是一个目录,它的数据块包括EXT2目录条目。
Home是这些目录之一,这个目录条描述/home目录的inode编号。必须读取这个目录(首先读取它的inode,然后读取从这个inode描述的数据块读取目录条目),从中查找rusling条目,从而得到描述/home/rusling目录的inode编号。最后,读取描述/home/rusling目录的inode指向的目录条目,找到.cshrc文件的inode编号,这样就得到了包含文件信息的数据块。
2.2.5 /proc文件系统
/proc文件系统是一个伪文件系统,它只存在内存中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用程序可以通过/proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息如进程是动态改变的,所以用户或应用程序读取/proc文件时,/proc文件系统是动态从系统内核读出所需信息并提交的。它的目录结构如表2-5所示。
表2-5 /proc文件系统的目录结构
并不是所有这些目录在系统中都有,这取决于内核配置和装载的模块。另外,在/proc下还有3个很重要的目录:net,scsi和sys。sys目录是可写的,可以通过它来访问或修改内核的参数(见后续介绍),而net和scsi则依赖于内核配置。例如,如果系统不支持scsi,则scsi目录不存在。
除了以上介绍的这些,还有的是一些以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的 PID 号为目录名,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口,是一个link。proc文件系统的名字就是由之而起。进程目录的结构如表2-6所示。
表2-6 进程目录的结构
用户如果要查看系统信息,可以用cat命令。例如:
[root@yangzongde fs]# cat /proc/filesystems nodev rootfs nodev bdev nodev proc nodev sockfs nodev tmpfs nodev shm nodev pipefs ext2 nodev ramfs iso9660 nodev devpts ext3 nodev usbdevfs nodev usbfs ntfs nodev autofs [root@yangzongde fs]#
用户还可以修改内核参数。在/proc文件系统中有一个目录/proc/sys。它不仅提供了内核信息,而且可以通过它修改内核参数来优化系统。但是必须很小心,因为如果修改不当可能会造成系统崩溃。
要改变内核的参数,只要用vi编辑或echo参数重定向到文件中即可。下面有一个例子。
# cat /proc/sys/fs/file-max 4096 # echo 8192 > /proc/sys/fs/file-max # cat /proc/sys/fs/file-max 8192
2.2.6 Linux文件操作函数
1.open/fopen打开文件
(1)open函数
函数定义:int open(const char *pathname,int flags)
功能说明:参数pathname为欲打开的文件路径字符串,flags所使用的旗标如表2-7所示。
表2-7 open函数的flags
库头文件:include<sys/tpyes.h>,include<sys/stat.h>,include<fcntl.h>
返回值:若所有欲核查的权限都通过则返回0,表示成功。只要有一个权限被禁止,则返回-1。
(2)fopen函数
函数定义:FILE *fopen(const char *path,const char *mode)
功能说明:参数path为欲打开文件的路径及文件名,参数mode字符串为打开形态。分别为:
r:打开只读文件,该文件必须存在。
r+:打开可读写的文件,此文件必须存在。
w:打开只写文件,该文件存在则长度清零,即内容消息,若不存在就创建。
w+:打开可读写文件,该文件存在则长度清零,即内容消息,若不存在就创建。
a:以附加方式打开只写文件,若文件不存在,就建立文件,如果文件存在,写入的数据会被加到文件后面。
a+:以附加方式打开可读写文件,若文件不存在,就建立文件,如果文件存在,写入的数据会被加到文件后面。
库头文件:include<stdio.h>
返回值:文件打开后,指向该流的文件指针就会被返回,若文件打开失败则返回NULL,错误代码写入errno中。
2.建立文件
creat函数
函数定义:int creat(const char *pathname,mode_tmode)
功能说明:参数pathname指向欲建立的文件路径,其相当于使用以下方式调用open函数。
open(const char *pathnaen,(O_CREAT|O_WRONLY|O_TRUNC),mode_t mode)
头文件:include<sys/tpyes.h>,include<sys/stat.h>,include<fcntl.h>
返回值:create()函数返回新的文件描述词,若有错误返回-1,并把错误代码设置为errno。相关的返回代码描述如下:
EEXIST:参数pathname所指的文件已经存在;
EACCESS:参数pathname所指定的文件不符合所要求测试的权限;
EROFS:欲打开写入权限的文件存在于只读文件系统中;
EFAULT:参数pathname指针超出可存取内存空间;
EINVAL:参数mode不正确;
ENAMETOOLONG:参数pathname太长;
ENOTDIR:参数pathname为一目录;
ENOMEM:核心内存不足;
ELOOP:参数pathname有过多符号连接问题;
EMFILE:已达到进程可同时打开的文件数上限;
ENFILE:已达到系统可以同时打开的文件数上限。
3.关闭文件
(1)close()函数
函数定义:int close(int fd)
功能说明:当使用文件后需要使用此函数来关闭打开的文件,从而让数据写回磁盘,释放系统资源,参数fd为打开的文件描述词。
头文件:#include<unistd.h>
返回值:正常执行返回0,否则返回-1。
(2)fclose()函数
函数定义:int fclose(FILE *stream)
功能说明:用来关闭由fopen()打开的文件,让缓冲区数据回写入磁盘中,并释放系统资源。
头文件:#include<stdio.h>
返回值:如果成功返回0,有错误发生则返回EOF并把错误代码存放到errno中。
4.读取数据
(1)read函数
函数定义:ssize_t read(int fd,void *buf,size_t count)
功能说明:read()把参数fd所指的文件传送count个字节到buf指针所指的内存中,若参数count为0,则read()不会有作用并返回0。返回值为实际读取到的字数。如果返回0,表示已经达到文件尾部或者无数据可读,另外文件读写位置会随读取到的字节移动。
头文件:#include<unistd.h>
(2)fread函数
函数定义:size_t fread(void *pth,size_t size,size_t nmemb,FILE *stream)
功能说明:从文件流读取数据,参数stream为已经打开的文件指针,参数ptr指向欲存放读取的数据空间,读取的字符以参数size*nmemb来决定,fread()会返回实际读取到的nmemb数目,如果此值比参数nmemb小,则代表可能读到了文件的尾部,这时必须用feof或者ferror来决定发生什么情况。
头文件:#include<stdio.h>
返回值:返回实际读取到的nmemb数目。
5.写入文件
(1)write函数
函数定义:ssize_t write(int fd,const void *buf,size_t count)
功能说明:将参数buf所指的内存写入count个字节到参数fd所指的文件内。
库头文件:#include<unistd.h>
返回值:如果顺序则返回实际写入的数据字节数,当有错误发生时则返回-1,错误代码存入errno中。
(2)fwrite函数
函数定义:size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream)
功能说明:将数据写入文件流中,参数stream为已经打开的文件指针,参数ptr指向欲写入的数据地址,总共写入的实际字符以参数size *nmemb来决定。
头文件:#include<stdio.h>
返回值:返回实际写入的nmemb数目。
6.移动文件的读写位置
(1)lseek()
函数定义:off_t lseek(int fildes,off_t offset,int whence)
功能说明:当打开文件时通常都有一个读写位置,通常是指向文件头部,若是以附加的方式打开文件,则在文件尾部,lseek用来控制文件的读写位置,参数fildes为已经打开的文件,offset为根据参数whence来移动读写位置的位移数。whence为下列其中一种。
SEEK_SET:参数offset即为新的读写位置;
SEEK_CUR:以目前的读写位置往后增加offset个位移量;
SEEK_END:将读写位置指向文件后再增加offset个位置量。
头文件:#include<unistd.h>
返回值:当执行成功则返回目前读写位置,也就是距离文件头部的字节数,若错误则返回-1。
(2)fseek()
函数定义:int fseek(FILE *stream,long offset,int whence)
功能说明:移动文件的读写位置,参数stream为已经打开的文件指针,参数offset为根据参数whence来移动读写位置的位移数。
头文件:#include<stdio.h>
返回值:如果成功返回1,否则返回0。
关于文件的其他操作,如dup/dup2复制文件、fcntl文件描述词操作、flock锁定文件或者解锁文件、fsync将缓冲区数据写回磁盘等本书将不再介绍,请读者参阅相关书籍。
2.3 存储管理
2.3.1 MTD内存管理
MTD(Memory Technology Device,内存技术设备)是用于访问memory设备(ROM、Flash)的Linux的子系统。MTD的主要目的是为了使新的memory设备的驱动更加简单,为此它在硬件和上层之间提供了一个抽象的接口。MTD 的所有源代码在/drivers/mtd子目录下,如图2-6所示。MTD设备分为四层(从设备节点直到底层硬件驱动),这四层从上到下依次是:设备节点、MTD设备层、MTD原始设备层和硬件驱动层。
图2-6 MTD设备层次
(1)Flash硬件驱动层。硬件驱动层负责在初始化时驱动Flash硬件,Linux MTD设备的NOR Flash芯片驱动遵循CFI接口标准,其驱动程序位于drivers/mtd/chips子目录下。NAND型Flash的驱动程序则位于/drivers/mtd/nand子目录下。
(2)MTD原始设备。原始设备层由两部分组成,一部分是MTD原始设备的通用代码,另一部分是各个特定的 Flash的数据,例如分区。用于描述 MTD 原始设备的数据结构是mtd_info,这其中定义了大量的关于MTD的数据和操作函数。mtd_table(mtdcore.c)则是所有MTD原始设备的列表,mtd_part(mtd_part.c)是用于表示MTD原始设备分区的结构,其中包含了mtd_info,因为每一个分区都是被看成一个MTD原始设备加在mtd_table中的, mtd_part.mtd_info中的大部分数据都从该分区的主分区mtd_part->master中获得。
在drivers/mtd/maps/子目录下存放的是特定的Flash的数据,每一个文件都描述了一块板子上的Flash。其中调用add_mtd_device()、del_mtd_device()建立/删除mtd_info结构并将其加入/删除mtd_table(或者调用add_mtd_partition()、del_mtd_partition()(mtdpart.c)建立/删除mtd_part结构并将mtd_part.mtd_info加入/删除mtd_table中)。
(3)MTD设备层:基于MTD原始设备,Linux系统可以定义出MTD的块设备(主设备号31)和字符设备(设备号90)。MTD字符设备的定义在mtdchar.c中实现,通过注册一系列file operation函数(lseek、open、close、read、write)。MTD块设备则是定义了一个描述MTD块设备的结构mtdblk_dev,并声明了一个名为mtdblks的指针数组,这数组中的每一个mtdblk_dev和mtd_table中的每一个mtd_info一一对应。
(4)设备节点:通过mknod在/dev子目录下建立MTD字符设备节点(主设备号为90)和MTD块设备节点(主设备号为31),通过访问此设备节点即可访问MTD字符设备和块设备。
(5)根文件系统:在Bootloader中将JFFS(或JFFS2)的文件系统映像jffs.image(或jffs2.img)烧到Flash的某一个分区中,在/arch/arm/mach-your/arch.c文件的your_fixup函数中将该分区作为根文件系统挂载。
(6)文件系统:内核启动后,通过mount命令可以将Flash中的其余分区作为文件系统挂载到mountpoint上。
一个MTD原始设备可以通过mtd_part分割成数个MTD原始设备并注册进mtd_table, mtd_table中的每个MTD原始设备都可以被注册成一个MTD设备,其中字符设备的主设备号为90,次设备号为0、2、4、6…(奇数次设备号为只读设备),块设备的主设备号为31,次设备号为0、1、2、3…。图2-7为设备层和原始设备层的函数调用关系。
图2-7 设备层和原始设备层的函数调用关系
1.NOR型Flash芯片驱动与MTD原始设备
所有的NOR型Flash的驱动(探测probe)程序都放在drivers/mtd/chips下,一个MTD原始设备可以由一块或者数块相同的Flash芯片组成,如图2-8所示。假设由4块devicetype为x8的Flash,每块大小为8M,interleave为2,起始地址为0x01000000,地址相连,则构成一个MTD原始设备(0x01000000-0x03000000),其中两块interleave成一个chip,其地址从0x01000000到0x02000000,另两块interleave成一个chip,其地址从0x02000000到0x03000000。
图2-8 Flash地址信息
请注意,所有组成一个MTD原始设备的Flash芯片必须是同类型的(无论是interleave还是地址相连),在描述MTD原始设备的数据结构中也只是采用了同一个结构来描述组成它的Flash芯片。
2.NAND和NOR的比较
NOR和NAND是现在市场上两种主要的非易失闪存技术。Intel于1988年首先开发出NOR Flash技术,彻底改变了原先由EPROM和EEPROM一统天下的局面。紧接着,1989年,东芝公司发表了NAND Flash结构,强调降低每比特的成本,提供更高的性能,并且像磁盘一样可以通过接口轻松升级。但是经过了十多年之后,仍然有相当多的硬件工程师分不清NOR和NAND闪存。“Flash存储器”经常可以与“NOR存储器”互换使用。许多业内人士也搞不清楚NAND闪存技术相对于NOR技术的优越之处,因为大多数情况下闪存只是用来存储少量的代码,这时NOR闪存更适合一些。而NAND则是高数据存储密度的理想解决方案。NOR的特点是芯片内执行(eXecute In Place , XIP),这样应用程序可以直接在Flash闪存内运行,不必再把代码读到系统RAM中。NOR的传输效率很高,在1~4MB的小容量时具有很高的成本效益,但是很低的写入和擦除速度大大影响了它的性能。
NAND结构能提供极高的单元密度,可以达到高存储密度,并且写入和擦除的速度也很快。应用NAND的困难在于Flash的管理需要特殊的系统接口。
3.性能比较
Flash闪存是非易失存储器,可以对称为块的存储器单元块进行擦写和再编程。由于任何 Flash器件的写入操作只能在空或已擦除的单元内进行,所以大多数情况下,在进行写入操作之前必须先执行擦除。NAND器件执行擦除操作是十分简单的,而NOR则要求在进行擦除前先要将目标块内所有的位都写为0。
由于擦除NOR器件时是以64~128KB的块进行的,执行一个写入/擦除操作的时间为5s,与此相反,擦除 NAND 器件是以8~32KB 的块进行的,执行相同的操作最多只需要4ms。
执行擦除时块尺寸的不同进一步拉大了NOR和NADN之间的性能差距,统计表明,对于给定的一套写入操作(尤其是更新小文件时),更多的擦除操作必须在基于NOR的单元中进行。这样,当选择存储解决方案时,设计师必须权衡以下的各项因素。
● NOR的读速度比NAND稍快一些。
● NAND的写入速度比NOR快很多。
● NAND的4ms擦除速度远比NOR的5s快。
● 大多数写入操作需要先进行擦除操作。
● NAND的擦除单元更小,相应的擦除电路更少。
(1)接口差别:NOR Flash带有SRAM接口,有足够的地址引脚来寻址,可以很容易地存取其内部的每一个字节。NAND器件使用复杂的I/O口来串行地存取数据,各个产品或厂商的方法可能各不相同。8个引脚用来传送控制、地址和数据信息。NAND读和写操作采用512 字节的块,这一点有点像硬盘管理此类操作,很自然地,基于 NAND 的存储器就可以取代硬盘或其他块设备。
(2)容量和成本:NAND Flash的单元尺寸几乎是NOR器件的一半,由于生产过程更为简单,NAND结构可以在给定的模具尺寸内提供更高的容量,也就相应地降低了价格。NOR Flash占据了容量为1~16MB闪存市场的大部分,而NAND Flash只是用在8~128MB的产品当中,这也说明NOR主要应用在代码存储介质中,NAND适合于数据存储,NAND在CompactFlash、Secure Digital、PC Cards和MMC存储卡市场上所占份额最大。
(3)可靠性和耐用性:采用Flash介质时一个需要重点考虑的问题是可靠性。对于需要扩展MTBF的系统来说,Flash是非常合适的存储方案。可以从寿命(耐用性)、位交换和坏块处理三个方面来比较NOR和NAND的可靠性。
(4)寿命(耐用性):在 NAND 闪存中每个块的最大擦写次数是一百万次,而 NOR的擦写次数是十万次。NAND存储器除了具有10比1的块擦除周期优势,典型的NAND块尺寸要比NOR器件小8倍,每个NAND存储器块在给定的时间内的删除次数要少一些。
(5)位交换:所有Flash器件都受位交换现象的困扰。在某些情况下(很少见,NAND发生的次数要比NOR多),一个比特位会发生反转或被报告反转了。一位的变化可能不很明显,但是如果发生在一个关键文件上,这个小小的故障可能导致系统停机。如果只是报告有问题,多读几次就可能解决了。当然,如果这个位真的改变了,就必须采用错误探测/错误更正(EDC/ECC)算法。位反转的问题更多见于NAND闪存,NAND的供应商建议使用NAND闪存的时候,同时使用EDC/ECC算法。这个问题对于用NAND存储多媒体信息时倒不是致命的。当然,如果用本地存储设备来存储操作系统、配置文件或其他敏感信息时,必须使用EDC/ECC系统以确保可靠性。
(6)坏块处理:NAND器件中的坏块是随机分布的。以前也曾有过消除坏块的努力,但发现成品率太低,代价太高,根本不划算。NAND器件需要对介质进行初始化扫描以发现坏块,并将坏块标记为不可用。在已制成的器件中,如果通过可靠的方法不能进行这项处理,将导致高故障率。
(7)易于使用:可以非常直接地使用基于NOR的闪存,可以像其他存储器那样连接,并可以在上面直接运行代码。由于需要I/O接口,NAND要复杂得多。各种NAND器件的存取方法因厂家而异。在使用 NAND 器件时,必须先写入驱动程序,才能继续执行其他操作。向 NAND 器件写入信息需要相当的技巧,因为设计师绝不能向坏块写入,这就意味着在NAND器件上自始至终都必须进行虚拟映射。
(8)软件支持:当讨论软件支持的时候,应该区别基本的读/写/擦操作和高一级的用于磁盘仿真和闪存管理算法的软件,包括性能优化。在 NOR 器件上运行代码不需要任何的软件支持,在 NAND 器件上进行同样操作时,通常需要驱动程序,也就是内存技术驱动程序(MTD),NAND 和 NOR 器件在进行写入和擦除操作时都需要 MTD。使用 NOR器件时所需要的MTD要相对少一些,许多厂商都提供用于NOR器件的更高级软件,这其中包括M-System的TrueFFS驱动,该驱动被Wind River System、Microsoft、QNX Software System、Symbian和Intel等厂商所采用。驱动还用于对DiskOnChip产品进行仿真和NAND闪存的管理,包括纠错、坏块处理和损耗平衡。
2.3.2 Linux内存管理
(1)alloca配置内存空间。
函数定义:void *alloca(size_t,size)
功能说明:alloca()用来配置size个字节的内存空间,alloca()是从堆栈空间(Stack)中配置内存,因此在函数返回时会自动释放空间。
库头文件:#include<stdlib.h>
返回值:若配置成功则返回指针,失败则返回NULL。
(2)calloc配置内存空间。
函数定义:void *calloc(size_t nmemb,size_t size)
功能说明:calloc()用来配置nmemb个相邻单位,每一个单位大小为size,并返回指向第一个元素的指针。
库头文件:#include<stdlib.h>
返回值:若配置成功则返回指针,失败则返回NULL。
(3)malloc配置内存空间。
函数定义:void malloc(size_t,size)
功能说明:malloc()用来配置内存空间大小,其大小由指定的size决定,如void*p=malloc(1024);
库头文件:#include<stdlib.h>
返回值:若配置成功则返回指针,失败则返回NULL。
(4)realloc更改已经配置的内存空间。
函数定义:void *realloc(void *ptr,size_t size)
功能说明:realloc用来更改已经配置的内存空间,参数ptr为指向先前由malloc、calloc和realloc所返回的内存指针,而参数size为新配置的内存大小。
库头文件:#include<stdlib.h>
返回值:若配置成功则返回指针,失败则返回NULL。
(5)getpagesize取得内存分页大小。
函数定义:size_t getpagesize(void)
功能说明: 返回内存一页的大小,单位为字节,此为系统的分页大小。
库头文件:#include<unistd.h>
返回值:内存分页的大小,在intel X86上为4096字节。
(6)free释放原先配置的内存。
函数定义:void free(void *ptr)
功能说明:参数ptr为指向先前由malloc等函数所返回的内存指针,调用此函数即收回这一部分内存空间。
库头文件:#include<stdlib.h>
2.4 设备管理
2.4.1 概述
Linux设备驱动程序属于Linux内核的一部分,并在Linux内核中扮演着十分重要的角色。它们像一个个“黑盒子”使某个特定的硬件响应一个定义良好的内部编程接口,同时完全隐蔽了设备的工作细节。用户通过一组标准化的调用来完成相关操作,这些标准化的调用是和具体设备驱动无关的,而驱动程序的任务就是把这些调用映射到具体设备对于实际硬件的特定操作上。
我们可以将设备驱动作为内核的一部分,直接编译到内核中,即静态链接,也可以单独作为一个模块(module)编译,在需要它的时候再动态地把它插入到内核中。在不需要时也可把它从内核中删除,即动态链接。显然动态链接比静态链接有更多的好处,但在嵌入式开发领域往往要求进行静态链接,尤其是像 S3C44B0 这种不带 MMU 的芯片。但在S3C2410等带MMU的ARM芯片中依然可以使用动态链接。
目前Linux支持的设备驱动可分为3种:字符设备(Character Device)、块设备(Block Deivce)和网络接口设备(Network Interface)。当然它们之间也并不是要严格加以区分。
字符设备:所有能够像字节流一样访问的设备,比如,文件等在Linux中都通过字符设备驱动程序来实现。在Linux中它们也被映射为文件系统的一个节点,常在/dev目录下。字符设备驱动程序一般要包含open、close、read、write等几个系统调用。
块设备:Linux的块设备通常是指诸如磁盘、内存、Flash等可以容纳文件系统的存储设备。与字符设备类似,块设备也是通过文件系统来进行访问,它们之间的区别仅仅在于内核内部管理数据的方式不同。它也允许像字符设备一样地访问,可以一次传递任意多的字节。Linux中的块设备包含整数个块,每个块包含2的几次幂的字节。
网络接口设备:网络接口设备是Linux中比较复杂的一种设备,通常它们指的是硬件设备,但有时也可是一个软件设备(如回环接口loopback)。它们由内核中网络子系统驱动,负责发送和接收数据包,而且它并不需要了解每一项事务是如何映射到实际传送的数据包的。由于它们的数据传送往往并不是面向流的(少数如telnet、FTP等是面向流的),所以不容易把它们映射到一个文件系统的节点上。在Linux中采用给网络接口设备分配一个唯一名字的方法来访问该设备。
2.4.2 字符设备与块设备
驱动程序在Linux内核中往往是以模块形式出现的。与应用程序的执行过程不同,模块通常只是预先向内核注册自己,当内核需要时响应请求。模块中包含两个重要的函数:init_module和cleanup_module。前者是模块的入口,它为模块调用做好准备工作,而后者则是在模块即将卸载时被调用,做一些清理工作。
驱动程序模块通过以下函数来完成向内核注册的。
int register_chrdev(unsigned major,const char *name,sturct file_operations *fops);
其中unsigned int major为主设备号,const char *name为设备名,至于结构指针struct file_operations *fops在驱动程序中十分重要,还要做详细介绍。
在 Linux中字符设备是通过文件系统中的设备名来进行访问的。这些名称通常放在/dev目录下,通过命令ls-l /dev可以看到该目录下的设备文件,其中第一个字母是“C”的为字符设备,而第一个字母是“b”的为块设备文件。其中每个设备文件都具有一个主设备号(major)和一个次设备号(minor)。当驱动程序调用open系统调用时,内核就是利用主设备号把该驱动与具体设备对应起来的。而内核并不关心次设备号,它是给主设备号已经确定的驱动程序使用的,一个驱动程序往往可以控制多个设备,如一个硬盘的多个分区,这时该硬盘拥有一个主设备号,而每个分区拥有自己的次设备号。2.0 以前版本的内核支持128个主设备号,在2.4版内核中已经增加到256个,但显然是不够的。计算机技术在迅猛发展,各种外设也是层出不穷,这样主设备号就越来越显得是一种稀缺资源了。这也就给实际开发造成麻烦,为了解决这一问题,在2.4版本以后的内核中引入了devfs设备文件系统,它提供一套自动管理设备文件的手段,使得设备文件的管理容易了许多。在编写好一个驱动程序模块后,按传统的主次设备号的方法来进行设备管理,则应手工为该模块建立一个设备节点。命令如下所示。
mknod /dev/ts c 254 0
其中/dev/ts表示设备名是ts,“C”说明它是字符设备,“254”是主设备号,“0”是次设备号。一旦通过mknod创建了设备文件,它就一直保留下来,除非手工删除它。这里要注意的是,在Linux内核中有许多设备号已经静态地赋予一些常用设备,剩下可用的设备号已经不多。如果我们的设备也随意找一个空闲的设备号,并进行静态编译的话,当其他的开发者也采用类似手段分配设备号,那很快就会造成冲突。如何解决这个问题呢?比较好的方法就是采用动态分配的方法。在用register_chrdev注册模块时,给major赋值为0,则系统就采用动态方式分配设备号。它会在所有未被使用的设备号中选定一个,作为函数返回值返回。一旦分配了设备号,就可以在/proc/devices中看到相关内容。/proc在前面关于操作系统移植的实验中已经提到,它是一个伪文件系统,它实际并不占用任何硬盘空间,而是在内核运行时在内存中动态生成的。它可以显示当前运行系统的许多相关信息。显然这一点对动态分配主设备号是非常有意义的。因为,正如前面提到的,采用主次设备号的方式管理设备文件,要在/dev目录下为设备创建一个设备名,可设备号却是动态产生的,每次都不一样,这样就不得不每次都重新运行一次mknod命令。这个过程通常通过编写自动执行脚本来完成,而其中的主设备号就可以通过/proc/devices中获得。当设备模块被卸载时,往往也会通过一个卸载脚本来显示删除/dev中相关设备名,这是一个比较好的习惯,因为内核包找不到相关设备文件,总比内核找到一个错误的驱动去执行要好得多。
前面已经提到了file_operations这个结构。内核就是通过这个结构来访问驱动程序的。在内核中对于一个打开的文件包括一个设备文件,都用file结构来标志,而常给出一个file_operations类型的结构指针,一般命名为fops来指向该file结构。可以说file与file_operations这两个结构就是驱动设备管理中最重要的两个结构。在file_operations结构中每个字段都必须指向驱动程序中实现特定的操作函数。对于不支持的操作,对应字段就被置为NULL。这样随着内核不断增加新功能,file_operations结构也就变得越来越庞大。现在的内核开发人员采用一种叫“标记化”的方法来为该结构进行初始化。即对驱动中用到的函数记录到相应字段中,没有用到的就不管。这样代码就精简了许多。
结构体file_operations在头文件linux/fs.h中定义的,在内核2.4 版内核中可以看到file_operations结构常是如下的一种定义:
struct file_operations { struct module *owner;//标示模块拥有者 loff_t (*llseek) (struct file *, loff_t, int); //loff_t 是一个64 位的长偏移数,llseek 方法标示当前文件的操作位置 ssize_t (*read) (struct file *, char *, size_t, loff_t *); //ssize_t(signed size)表示当前平台的固有整数类型。Read 是读函数 ssize_t (*write) (struct file *, const char *, size_t, loff_t *); //写函数 int (*readdir) (struct file *, void *, filldir_t); //readdir 方法用于读目录,其只对文件系统有效 unsigned int (*poll) (struct file *, struct poll_table_struct *); //该方法用于查询设备是否可读,可写或处于某种状态。当设备不可读写时它们可以被阻塞直至设备 变为可读或可写。如果驱动程序中没有定义该方法则它驱动的设备就会被认为是可读写的 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //ioctl 是一个系统调用,它提供了一种执行设备特定命令的方法 int (*mmap) (struct file *, struct vm_area_struct *); //该方法请求把设备内存映射到进程地址空间 int (*open) (struct inode *, struct file *); //即打开设备文件,它往往是设备文件执行的第一个操作 int (*flush) (struct file *); //进程在关闭设备描述符副本之前会调用该方法,它会执行设备上尚未完成的操作 int (*release) (struct inode *, struct file *); //当file 结构被释放时就会调用该方法 int (*fsync) (struct file *, struct dentry *, int datasync); //该方法用来刷新待处理的数据 int (*fasync) (int, struct file *, int); //即异步通知它是比较高级功能这里不作介绍 int (*lock) (struct file *, int, struct file_lock *); //该方法用来实现文件锁定 ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); //应用程序有时需要进行涉及多个内存区域的单次读写操作,利用该方法以及下面的writev 可 以完成这类操作 ssize_t (*writev)(struct file *, const struct iovec *, unsigned long, loff_t *); };
前面已经提到,目前采用的是“标记化”方法来为该结构赋值。下面要给出的代码中可以看到如下一段。
static struct file_operations s3c44b0_fops = { owner: THIS_MODULE, open: s3c44b0_ts_open, read: s3c44b0_ts_read, release: s3c44b0_ts_release, poll: s3c44b0_ts_poll, };
它只对需要的函数赋值,对不需要的没有进行操作。这样使得代码结构更为清晰。
下面要讲到的是另一个重要的结构——file,它也定义在头文件linux/fs.h中。它代表一个打开的文件,由内核在调用open时创建。并传递给在该文件上进行操作的所有函数,直到最后的close函数被调用。在文件的所有实例都关闭时,内核释放这个数据结构。下面对其中一些重要字段做一些解释。
mode_t f_mode:该字段表示文件模式,它通过FMODE_READ 和FMODE_WRITE 位来标示文件是否可读,可写。
loff_t f_pos:该字段标示文件当前读写位置。
unsigned int_f_flags:这是文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC 等,驱动程序为了支持非阻塞型操作需要检查这个标志。
struct file_operations *f_op:这就是对前面介绍的file_operations结构的操作。内核在执行open操作时对这个指针赋值,以后需要处理这些操作时就读取这个指针。
void *private_data:这是个应用非常灵活的字段,驱动可以把它应用于任何目的,可以将它指向已经分配的数据,但一定要在内核销毁file结构前在release方法中释放该内存。
struct dentry *f_dentry:它对应一个目录项结构,是一种优化的设计。
2.4.3 主设备号和次设备号
传统方式中的设备管理中,除了设备类型外,内核还需要一对称做主次设备号的参数,才能唯一标识一个设备。主设备号相同的设备使用相同的驱动程序,次设备号用于区分具体设备的实例。
例如,PC中的IDE设备,一般主设备号使用3,Windows下进行的分区,一般将主分区的次设备号为1,扩展分区的次设备号为2、3、4,逻辑分区使用5、6等。
设备操作宏MAJOR()和MINOR()可分别用于获取主次设备号,宏MKDEV()用于将主设备号和次设备号合并为设备号,这些宏定义在include/linux/kdev_t.h中。
[root@yangzongde root]# cat /usr/src/linux-2.4.20-8/include/linux/kdev_ t.h ...... #define MINORBITS 8 #define MINORMASK ((1U << MINORBITS) - 1) typedef unsigned short kdev_t; #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define HASHDEV(dev) ((unsigned int) (dev)) #define NODEV 0 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) #define B_FREE 0xffff /* yuk */ ......
对于Linux中对设备号的分配原则可以参考Documentation/devices.txt。
对于查看/dev目录下的设备的主次设备号可以使用如下命令:
[root@yangzongde root]# ls -l /dev/ |more
总用量228 crw------- 1 root root 10, 10 2003-01-30 adbmouse crw-r--r-- 1 root root 10, 175 2003-01-30 agpgart crw------- 1 root root 10, 4 2003-01-30 amigamouse crw------- 1 root root 10, 7 2003-01-30 amigamouse1 crw------- 1 root root 10, 134 2003-01-30 apm_bios drwxr-xr-x 2 root root 4096 3ÔÂ 22 05:30 ataraid crw------- 1 root root 10, 5 2003-01-30 atarimouse crw------- 1 root root 10, 3 2003-01-30 atibm crw------- 1 root root 10, 3 2003-01-30 atimouse crw------- 1 root root 14, 4 2003-01-30 audio crw------- 1 root root 14, 20 2003-01-30 audio1 crw------- 1 root root 14, 7 2003-01-30 audioctl brw-rw---- 1 root disk 29, 0 2003-01-30 aztcd crw------- 1 root root 10, 128 2003-01-30 beep brw-rw---- 1 root disk 41, 0 2003-01-30 bpcd crw------- 1 root root 68, 0 2003-01-30 capi20 crw------- 1 root root 68, 1 2003-01-30 capi20.00 crw------- 1 root root 68, 2 2003-01-30 capi20.01 crw------- 1 root root 68, 3 2003-01-30 capi20.02 crw------- 1 root root 68, 4 2003-01-30 capi20.03 crw------- 1 root root 68, 5 2003-01-30 capi20.04 ......
设备类型、主次设备号是内核与设备驱动程序通信时所使用的,但是由于对于开发应用程序的用户来说比较难于理解和记忆,所以Linux使用了设备文件的概念来统一对设备的访问接口,在引入设备文件系统(devfs)之前Linux将设备文件放在/dev目录下,设备的命名一般为设备文件名+数字或字母表示的子类,例如/dev/hda1、/dev/hda2等。
在Linux 2.4及以后内核中引入了设备文件系统(devfs),所有的设备文件作为一个可以挂装的文件系统,这样就可以被文件系统进行统一管理,从而设备文件就可以挂装到任何需要的地方。命名规则也发生了变化,一般将主设备建立一个目录,再将具体的子设备文件建立在此目录下。
2.5 本章总结
用户要进行程序开发,熟悉系统开发环境是非常重要的。本章重点介绍了Linux系统开发的环境平台,主要包括进程/线程管理、文件系统结构和类型、存储管理、设备管理等知识。通过本章的学习,读者对Linux系统开发的环境平台有较深刻的了解,有助于在以后的程序设计和系统开发中更加得心应手。