===========================================================================
1.系统调用
什么是系统调用:
由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。
C标准函数和系统函数调用关系。一个helloworld如何打印到屏幕。
2.C标准库文件IO函数
fopen、fclose、fseek、fgets、fputs、fread、fwrite......
r 只读、 r+读写
w只写并截断为0、 w+读写并截断为0
a追加只写、 a+追加读写
3.open/close函数
3.1 函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int close(int fd);
3.2 常用参数
O_RDONLY、O_WRONLY、O_RDWR
O_APPEND、O_CREAT、O_EXCL、 O_TRUNC、 O_NONBLOCK
创建文件时,指定文件访问权限。权限同时受umask影响。结论为:
文件权限 = mode & ~umask
都变成二进制,掩码取反,然后与运算.
使用头文件:<fcntl.h>
3.3 open常见错误
1. 打开文件不存在
2. 以写方式打开只读文件(打开文件没有对应权限)
3. 以只写方式打开目录
4.文件描述符
4.1 PCB进程控制块
PCB进程控制块:本质就是结构体,其中一个成员就是文件描述符表。文件描述符表中的每一个成员都是文件描述符。该表中能用的就是最小的。
0-STDIN_FILENO
1-STDOUT_FILENO
2-STDERR_FILENO
可使用命令locate sched.h查看位置:
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h
struct task_struct
{
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
/*
表示进程的当前状态:
TASK_RUNNING:正在运行或在就绪队列run-queue中准备运行的进程,实际参与进程调度。
TASK_INTERRUPTIBLE:处于等待队列中的进程,待资源有效时唤醒,也可由其它进程通过信号(signal)或定时中断唤醒后进入就绪队列run-queue。
TASK_UNINTERRUPTIBLE:处于等待队列中的进程,待资源有效时唤醒,不可由其它进程通过信号(signal)或定时中断唤醒。
TASK_ZOMBIE:表示进程结束但尚未消亡的一种状态(僵死状态)。此时,进程已经结束运行且释放大部分资源,但尚未释放进程控制块。
TASK_STOPPED:进程被暂停,通过其它进程的信号才能唤醒。导致这种状态的原因有二,或者是对收到SIGSTOP、SIGSTP、SIGTTIN或SIGTTOU信号的反应,或者是受其它进程的ptrace系统调用的控制而暂时将CPU交给控制进程。
TASK_SWAPPING: 进程页面被交换出内存的进程。
*/
unsigned long flags; //进程标志,与管理有关,在调用fork()时给出
int sigpending; //进程上是否有待处理的信号
mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同
/*用户线程空间地址: 0..0xBFFFFFFF。
内核线程空间地址: 0..0xFFFFFFFF */
struct exec_domain *exec_domain; //进程执行域
volatile long need_resched; //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
unsigned long ptrace;
int lock_depth; //锁深度
long counter; //进程的基本时间片,在轮转法调度时表示进程当前还可运行多久,在进程开始运行是被赋为priority的值,以后每隔一个tick(时钟中断)递减1,减到0时引起新一轮调 度。重新调度将从run_queue队列选出counter值最大的就绪进程并给予CPU使用权,因此counter起到了进程的动态优先级的作用
long nice; //静态优先级
unsigned long policy; //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR,分时进程:SCHED_OTHER
//在Linux 中, 采用按需分页的策略解决进程的内存需求。task_struct的数据成员mm 指向关于存储管理的mm_struct结构。
struct mm_struct *mm; //进程内存管理信息
int has_cpu, processor;
unsigned long cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned long sleep_time; //进程的睡眠时间
//用于将系统中所有的进程连成一个双向循环链表,其根是init_task
//在Linux 中所有进程(以PCB 的形式)组成一个双向链表,next_task和prev_task是链表的前后向指针
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm; //active_mm 指向活动地址空间。
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; //父进程终止是向子进程发送的信号
unsigned long personality;
int dumpable:1;
int did_exec:1;
pid_t pid; //进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
int leader; //表示进程是否为会话主管
//指向最原始的进程任务指针,父进程任务指针,子进程任务指针,新兄弟进程任务指针,旧兄弟进程任务指针。
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct list_head thread_group; //线程链表
//用于将进程链入HASH表,系统进程除了链入双向链表外,还被加入到hash表中
struct task_struct *pidhash_next;
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct semaphore *vfork_sem; //供vfork()使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies,系统根据it_real_value
//设置定时器的第一个终止时间.在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据it_virt_incr重置初值
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; //记录进程消耗的时间
unsigned long start_time; //进程创建的时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];//记录进程在每个CPU上所消耗的用户态时间和核心态时间
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copyon Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数);nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt,cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid,euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
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; //代表进程所属的用户
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned short used_math; //是否使用FPU
char comm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count;
struct tty_struct *tty; //进程所在的控制终端,如果不需要控制终端,则该指针为空
unsigned int locks; /* How many file locks are being held */
//进程间通信信息
struct sem_undo *semundo; //进程在信号量上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号量操作而挂起时,他在该队列中记录等待的操作
struct thread_struct thread; //进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct fs_struct *fs; //文件系统信息,fs保存了进程本身与VFS(虚拟文件系统)的关系信息
struct files_struct *files; //打开文件信息,指向文件描述符号
//信号处理函数
spinlock_t sigmask_lock; /* Protects signal and blocked */
struct signal_struct *sig; //信号处理函数
sigset_t 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;
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock; //用于申请空间时用的自旋锁。自旋锁的主要功能是临界区保护
};
4.2 文件描述图表
结构体PCB 的成员变量file_struct *file 指向文件描述符表。
从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标0/1/2/3/4…找到文件结构体。
本质是一个键值对0、1、2…都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。
新打开文件返回文件描述符表中未使用的最小文件描述符。
STDIN_FILENO 0
STDOUT_FILENO 1
STDERR_FILENO 2
4.3 最大打开文件数
一个进程默认打开文件的个数1024。
命令查看ulimit -a 查看open files 对应值。默认为1024
可以使用ulimit -n 4096 修改
当然也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。
cat /proc/sys/fs/file-max可以查看该电脑最大可以打开的文件个数。
受内存大小影响。
4.4 FIFE结构体
主要包含文件描述符、文件读写位置、IO缓冲区三部分内容。
struct file {
...
文件的偏移量;
文件的访问权限;
文件的打开标志;
文件内核缓冲区的首地址;
struct operations * f_op;
...
};
查看方法:
(1) /usr/src/linux-headers-3.16.0-30/include/linux/fs.h
(2) lxr:百度 lxr → lxr.oss.org.cn → 选择内核版本(如3.10) → 点击File Search进行搜索
→ 关键字:“include/linux/fs.h” → Ctrl+F 查找 “struct file {”
→ 得到文件内核中结构体定义
→ “struct file_operations”文件内容操作函数指针
→ “struct inode_operations”文件属性操作函数指针
5. read/write 函数
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read与write函数原型类似。使用时需注意:read/write函数的第三个参数。
练习:编写程序实现简单的cp功能。
程序比较:如果一个只读一个字节实现文件拷贝,使用read、write效率高,还是使用对应的标库函数(fgetc、fputc)效率高呢?
5.1 strace命令
shell中使用strace命令跟踪程序执行,查看调用的系统函数。
5.2 缓冲区
read、write函数常常被称为Unbuffered I/O。指的是无用户及缓冲区。但不保证不使用内核缓冲区。
5.3 预读入缓输出
6.错误处理函数
错误号:errno
perror函数: void perror(const char *s);
strerror函数: char *strerror(int errnum);
查看错误号:
/usr/include/asm-generic/errno-base.h
/usr/include/asm-generic/errno.h
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
7.阻塞、非阻塞
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:
正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。
阻塞读终端: 【block_readtty.c】
非阻塞读终端 【nonblock_readtty.c】
非阻塞读终端和等待超时 【nonblock_timeout.c】
注意,阻塞与非阻塞是对于文件而言的。而不是read、write等的属性。read终端,默认阻塞读。
总结read 函数返回值:
1. 返回非零值: 实际read到的字节数
2. 返回-1: 1):errno != EAGAIN (或!= EWOULDBLOCK) read出错
2):errno == EAGAIN (或== EWOULDBLOCK) 设置了非阻塞读,并且没有数据到达。
3. 返回0:读到文件末尾
附上测试代码:
阻塞方式打开设备文件:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char buf[10];
int n;
n=read(STDIN_FILENO,buf,10);
if(n<0){
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO,buf,n);
return 0;
}
非阻塞方式打开设备文件:
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char buf[10];
int fd,n;
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0){
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd,buf,10);
if(n<0){
if(errno != EAGAIN ){ //if(errno!=EWOULDBLOCK)
perror("read /dev/tty");
exit(1);
}else{
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2);
goto tryagain;
}
}
write(STDOUT_FILENO,buf,n);
close(fd);
return 0;
}
非阻塞设置超时:
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"
int main(void)
{
char buf[10];
int fd,n,i;
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0){
perror("open /dev/tty");
exit(1);
}
printf("open /dev/tty ok... %d\n",fd);
for(i=0;i<5;i++){
n = read(fd,buf,10);
if(n<0){
if(errno != EAGAIN ){ //if(errno!=EWOULDBLOCK)
perror("read /dev/tty");
exit(1);
}else{
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
sleep(2);
}
}
}
if(i==5){
write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
}else{
write(STDOUT_FILENO,buf,n);
}
close(fd);
return 0;
}
8.lseek函数
8.1 文件偏移
Linux中可使用系统函数lseek来修改文件偏移量(读写位置)
每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。
回忆fseek的作用及常用参数。 SEEK_SET、SEEK_CUR、SEEK_END
int fseek(FILE *stream, long offset, int whence); 成功返回0;失败返回-1
特别的:超出文件末尾位置返回0;往回超出文件头位置,返回-1
off_t lseek(int fd, off_t offset, int whence); 失败返回-1;成功:返回的值是较文件起始位置向后的偏移量。
特别的:lseek允许超过文件结尾设置偏移量,文件会因此被拓展。
注意文件“读”和“写”使用同一偏移位置。
【lseek.c】
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(void){
int fd,n;
char msg[]="it's a test foe lseek\n";
char ch;
fd = open("lseek.txt",O_RDWR|O_CREAT,0644);
if(fd<0)
{
perror("open lseek.txt error");
exit(1);
}
write(fd,msg,strlen(msg));
lseek(fd,0,SEEK_SET);
while((n=read(fd,&ch,1))){
if(n<0){
perror("read error");
exit(1);
}
write(STDOUT_FILENO,&ch,n);
}
close(fd);
return 0;
}
~
8.2 lseek常用应用
1. 使用lseek拓展文件:write操作才能实质性的拓展文件。单lseek是不能进行拓展的。
一般:write(fd, "a", 1);
od -tcx filename 查看文件的16进制表示形式
od -tcd filename 查看文件的10进制表示形式
使用truncate函数,直接拓展文件。
2. 通过lseek获取文件的大小:lseek(fd, 0, SEEK_END); 【lseek_test.c】
【最后注意】:lseek函数返回的偏移量总是相对于文件头而言。
【lseek_test.c】
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
int fd = open(argv[1],O_RDWR);
if(fd == -1)
{
perror("open error");
exit(1);
}
int lenth = lseek(fd,0,SEEK_END);
printf("file size:%d\n",lenth);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc,char *argv[])
{
//open/lseek(fd,249,SEEK_END)/write(fd,"\0",1);
int ret =truncate("lseek.txt",250);//in there ,it must own an knowned file.
printf("ret = %d\n",ret);
return 0;
}
9.fcntl函数
改变一个【已经打开】的文件的 访问控制属性。
重点掌握两个参数的使用,
F_GETFL 和 F_SETFL。
fcntl :
int flgs =fcntl(fd,F_GETFL);
获取文件状态:F_GETFL
设置文件状态:F_SETFL
位图:
位图就是使用一个比特位来进行表示数据存在与否的信息。
这里面的flags就是位图
【fcntl.c】
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#define MSG_TRY "try again\n"
int main(void)
{
char buf[10];
int flags ,n;
flags = fcntl(STDIN_FILENO,F_GETFL);
if(flags == -1){
perror("fcntl error");
exir(1);
}
flags |= O_NONBLOCK;
int ret = fcntl(STDIN_FILENO,F_SETFL,flags);
if(ret == -1){
perror("fcntl error");
exit(1);
}
tryagain:
n=read(STDIN_FILENO,buf,10);
if(n<0){
if(errno != EAGAIN){
perror("read /dev/tty");
exit(1);
}
sleep(3);
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
goto tryagain;
}
write(STDOUT_FILENO,buf,n);
return 0;
}
10.ioctl 函数
对设备的I/O通道进行管理,控制设备特性。(主要应用于设备驱动程序中)。
通常用来获取文件的【物理特性】(该特性,不同文件类型所含有的值各不相同)
11.传入输出参数
11.1传入参数
const 关键字修饰的 指针变量 在函数内部读操作。 char *strcpy(cnost char *src, char *dst);
11.2 传出参数
1. 指针做为函数参数
2. 函数调用前,指针指向的空间可以无意义,调用后指针指向的空间有意义,且作为函数的返回值传出
3. 在函数内部写操作。
11.3 传入传出参数
传入参数:
1.指针作为函数参数
2.通常const关键字修饰
3.指针指向有效区域,在函数内部做读操作
传出参数:
1.指针作为函数参数
2.在函数调用之前,指针指向的空间可以无意义,但必须有效
3.在函数内部,做写操作。
4.函数调用结束后,充当函数返回值。
传入传出参数:
1.指针作为函数参数
2.在函数调用之前,指针有实际意义
3.在函数内部,先做读操作,后做写操作
4.函数调用结束后,充当函数返回值。
拓展阅读
关于虚拟4G内存的描述和解析: 一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了4G。而用到的内存区域,会通过页表映射到物理内存。
所以每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是不同的。内核用的是3G以上的1G虚拟内存地址, 其中896M是直接映射到物理地址的,128M按需映射896M以上的所谓高位内存。各进程使用的是同一个内核。
首先要分清“可以寻址”和“实际使用”的区别。
其实我们讲的每个进程都有4G虚拟地址空间,讲的都是“可以寻址”4G,意思是虚拟地址的0-3G对于一个进程的用户态和内核态来说是可以访问的,而3-4G是只有进程的内核态可以访问的。并不是说这个进程会用满这些空间。
其次,所谓“独立拥有的虚拟地址”是指对于每一个进程,都可以访问自己的0-4G的虚拟地址。虚拟地址是“虚拟”的,需要转化为“真实”的物理地址。
好比你有你的地址簿,我有我的地址簿。你和我的地址簿都有1、2、3、4页,但是每页里面的实际内容是不一样的,我的地址簿第1页写着3你的地址簿第1页写着4,对于你、我自己来说都是用第1页(虚拟),实际上用的分别是第3、4页(物理),不冲突。
内核用的896M虚拟地址是直接映射的,意思是只要把虚拟地址减去一个偏移量(3G)就等于物理地址。同样,这里指的还是寻址,实际使用前还是要分配内存。而且896M只是个最大值。如果物理内存小,内核能使用(分配)的可用内存也小。