[1 Unix基础]

源码对应intro目录

1.2 Unix体系架构

内核的接口被称为系统调用(system call)。公用函数库构建在系统调用接口之上,应用程序即可使用公用函数库,也可使用系统调用。shell是一个特殊的应用程序,为运行其他应用程序提供了一个接口。

图1-1 Unix操作系统的体系结构

1.3 登录

系统在口令文件(/etc/passwd)中查看登录名。口令文件的登录项由7个以冒号分隔的字段组成,依次是:登录名,加密口令,数字用户ID(205),数字组ID(105),注释字段,起始目录(/home/sar)以及shell程序(/bin/ksh)。

sar:x:205:105:Stephen:/home/sar:/bin/ksh

系统已经将加密口令(x)移动到了另一个文件中,第6章将说明这种文件以及访问它们的函数。

1.4 文件和目录

1 文件系统

stat和fstat函数返回包含所有文件属性的一个信息结构,第4章将详细说明。

2 文件名

只有斜线(/)和空字符不能出现在文件名中。斜线用来分隔构成路径名的各文件名,空字符则用来终止一个文件名。

3 目录

列出一个目录中所有文件的名字,ls(1):1表示后面接1个参数

#include "apue.h"
#include <dirent.h>

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

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

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

	closedir(dp);
	exit(0);
}

dirent.h,使用opendir和readdir函数原型,以及dirent结构的定义。

exit,0表示正常结束,参数值1~255表示出错。

1.5 输入和输出

1 文件描述符

文件描述符通常是小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。

2 标准输入、标准输出和标准错误

每当运行一个程序,shell为其打开3个文件描述符,即标准输入、标准输出和标准错误。默认,这3个描述符都链接向终端。shell可以使这3个描述符重新定向到某个文件:

ls > file

标准输出重定向到file文件。

3 不带缓冲的I/O

函数open、read、write、lseek以及close提供了不带缓冲的I/O。

从标准输入读,并向标准输出写。程序用于复制任一Unix文件,类似cat。

#include "apue.h"

#define	BUFFSIZE	4096

int main(void)
{
	int		n;
	char	buf[BUFFSIZE];

	while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
		if (write(STDOUT_FILENO, buf, n) != n)
			err_sys("write error");

	if (n < 0)
		err_sys("read error");

	exit(0);
}

头文件<unistd.h>包含read和write函数声明,STDIN_FILENO(0),STDOUT_FILENO(1)。

read函数返回读取的字节数。当到达输入文件的末尾时,read返回0,程序停止运行。如果发生了一个读错误,read返回-1。

./mycat > data

标准输入是终端,标准输出重定向到文件data,标准错误是终端。该程序将用户输入的各行复制到标准输出,输入文件结束符(ctrl+d)后终止。
 

./mycat < infile > outfile

标准输入重定向到infile,标准输出重定向到outfile。该程序会将infile文件的内容复制到outfile文件中。

4 标准I/O

标准I/O为那些不带缓冲的I/O提供了带缓冲的接口。使用标准I/O无需考虑如何选取最佳的缓冲区大小,如3中的BUFFSIZE;同时简化了对输入行的处理,例如,fgets函数读取一行而read函数读取指定字节数。

将标准输入复制到标准输出,也能复制任一Unix文件:

#include "apue.h"

int main(void)
{
	int		c;

	while ((c = getc(stdin)) != EOF)
		if (putc(c, stdout) == EOF)
			err_sys("output error");

	if (ferror(stdin))
		err_sys("input error");

	exit(0);
}

getc一次读取一个字符,putc将此字符写到标准输出。读到输入的最后一个字节时,getc返回常量EOF。<stdio.h>中包含stdin,stdout和EOF(通常为-1)的定义。

1.6 程序和进程

内核使用exec函数(7个exec函数之一),将程序读入内存,并执行程序。

3 进程控制

有3个用于进程控制功能的主要函数:fork、exec和waitpid。

该程序从标准输入读取命令,并执行命令。类似shell。

#include "apue.h"
#include <sys/wait.h>

int main(void)
{
	char	buf[MAXLINE];	/* from apue.h */
	pid_t	pid;
	int		status;

	printf("%% ");	/* print prompt (printf requires %% to print %) */
	while (fgets(buf, MAXLINE, stdin) != NULL) {
		if (buf[strlen(buf) - 1] == '\n')
			buf[strlen(buf) - 1] = 0; /* replace newline with null */

        printf("parent getpid:%ld \n", (long)getpid());
		if ((pid = fork()) < 0) {
			err_sys("fork error");
		} else if (pid == 0) {		/* child */
            printf("child pid:%d \n", pid);
            printf("child getpid:%ld \n", (long)getpid());
			execlp(buf, buf, (char *)0);
            // todo
			err_ret("couldn't execute: %s getpid:%ld", buf, getpid());
			exit(127);
		}

        printf("parent wait for pid:%d \n", pid);
		/* parent */
		if ((pid = waitpid(pid, &status, 0)) < 0)
			err_sys("waitpid error");
		printf("%% ");
	}
	exit(0);
}

子进程正常退出后todo后两句没有运行,可能的解释:

1 正常情况,子进程execlp里exit(0)会退出进程。

2 异常情况,子进程没有exit(0),需要用户exit(>0)表示异常退出。

解释如上程序:

1 用标准I/O函数fgets从标准输入一次读取一行。当输入文件结束符(ctrl+d)时,fgets返回null,循环停止。

2 fgets返回的每一行是以换行符终止,需要用null替换。这是因为execlp函数要求的参数是以null结束的而不是以换行符结束的。

int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);

其中第一个参数file指向可执行文件名称,因为execlp()函数会从系统PATH路径中寻找相应命令,所以不需要带完整路径;第二个参数之后就都是传给可执行文件的参数,类似main函数的args[],只是最后一个参数必须为空字符指针。

3 fork创建一个新进程,被调用一次(被父进程),返回两次(在父进程和子进程中)。

4 在子进程中,调用execlp以执行从标准输入读入的命令。这就用新的程序文件替换了子进程原来执行的程序文件。

5 父进程通过waitpid来等待子进程终止,参数指定要等待的进程(pid参数是子进程pid)。

6 该程序的限制是不能想执行的命令传递参数。

1.7 出错处理

Unix系统函数出错时,通常会返回一个负值,且整形变量errno通常被设置为具有特定信息的值。例如,open函数如果成功返回一个非负文件描述符,如果错误返回-1。open出错时,有大约15种errno值(文件不存在,权限问题等)。

linux错误码:

linux错误码大全_zhangwentaohh的专栏-CSDN博客_linux错误码

errno应注意两条规则。

1 如果没有出错,其值不会被清除。因此,仅当函数的返回值指明出错时,才检验其值。
2 任何函数不会将errno设置为0,且<errno.h>中定义的错误码都不为0。(一般>0)

C标准定义了两个函数,用于打印出错信息:

1 strerror

#include <string.h>

// 返回值:指向消息字符串的指针
char *strerror(int errnum);

strerror函数将errnum映射为一个出错消息字符串。

todo:返回的char*堆内存何时free???贴下strerror源码:

/*** 
*char *strerror(errnum) - Map error number to error message string. 
* 
*Purpose: 
*       The strerror runtime takes an error number for input and 
*       returns the corresponding error message string.  This routine 
*       conforms to the ANSI standard interface. 
* 
*Entry: 
*       int errnum - Integer error number (corresponding to an errno value). 
* 
*Exit: 
*       char * - Strerror returns a pointer to the error message string. 
*       This string is internal to the strerror routine (i.e., not supplied 
*       by the user). 
* 
*Exceptions: 
*       None. 
* 
*******************************************************************************/  
  
#ifdef _UNICODE  
wchar_t * cdecl _wcserror(  
#else  /* _UNICODE */  
char * __cdecl strerror (  
#endif  /* _UNICODE */  
        int errnum  
        )  
{  
        _TCHAR *errmsg;  
        _ptiddata ptd = _getptd_noexit();  
        if (!ptd)  
                return _T("Visual C++ CRT: Not enough memory to complete call to strerror.");  
  
        if ( (ptd->_terrmsg == NULL) && ((ptd->_terrmsg =  
                        _calloc_crt(_ERRMSGLEN_, sizeof(_TCHAR)))  
                        == NULL) )  
                return _T("Visual C++ CRT: Not enough memory to complete call to strerror.");  
        else  
                errmsg = ptd->_terrmsg;  
  
#ifdef _UNICODE  
        _ERRCHECK(mbstowcs_s(NULL, errmsg, _ERRMSGLEN_, _get_sys_err_msg(errnum), _ERRMSGLEN_ - 1));  
#else  /* _UNICODE */  
        _ERRCHECK(strcpy_s(errmsg, _ERRMSGLEN_, _get_sys_err_msg(errnum)));  
#endif  /* _UNICODE */  
  
        return(errmsg);  
}  

2 perror

#include <stdio.h>

void perror(const char *msg);

perror先输出msg指向的字符串,再基于errno的当前值在标准错误上输出一条出错信息。

说明两个出错函数的使用方法:

#include "apue.h"
#include <errno.h>

int main(int argc, char *argv[])
{
	fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
	errno = ENOENT;
	/* errno = EACCES; */
	perror(argv[0]);
	exit(0);
}

测试结果:
EACCES: Permission denied
./testerror: No such file or directory

注意:将程序名(argv[0])作为参数传递给perror。这是使用惯例。使用这种方法,在程序作为管道时,就可以知道是哪个程序产生了出错信息了:

prog1 | prog2 | prog3

1.8 用户标识

3 附属组ID

/etc/group文件可以看出某个组里包含哪些用户,参考:

linux添加用户到附属组无权访问_麻麻兔的博客-CSDN博客

Unix系统是允许一个用户属于多个组的。

1.9 信号

信号用于通知进程发生了某种情况。例如,若某一进程执行除法操作,若除数为0则将名为SIGFPE(浮点异常)的信号发送给该进程。进程有以下3种处理信号的方式:

1 忽略信号。

2 按照系统默认方式处理。如除数0,系统默认方式是终止进程。

3 提供一个函数,信号发生时调用该函数,即捕捉信号。

产生信号:

1 中断键ctrl+c,对应信号SIGINT,系统默认动作是终止进程。

2 退出键ctrl+\

3 kill函数,在一个进程中调用此函数可以向另一个进程发送信号。

如下程序,为了能捕捉信号,程序调用signal函数,其中指定了当产生SIGINT信号时要调用的函数的名字sig_int。

#include "apue.h"
#include <sys/wait.h>

static void	sig_int(int);		/* our signal-catching function */

int main(void)
{
	char	buf[MAXLINE];	/* from apue.h */
	pid_t	pid;
	int		status;

	if (signal(SIGINT, sig_int) == SIG_ERR)
		err_sys("signal error");

	printf("%% ");	/* print prompt (printf requires %% to print %) */
	while (fgets(buf, MAXLINE, stdin) != NULL) {
		if (buf[strlen(buf) - 1] == '\n')
			buf[strlen(buf) - 1] = 0; /* replace newline with null */

		if ((pid = fork()) < 0) {
			err_sys("fork error");
		} else if (pid == 0) {		/* child */
			execlp(buf, buf, (char *)0);
			err_ret("couldn't execute: %s", buf);
			exit(127);
		}

		/* parent */
		if ((pid = waitpid(pid, &status, 0)) < 0)
			err_sys("waitpid error");
		printf("%% ");
	}
	exit(0);
}

void sig_int(int signo)
{
    // 有问题
	printf("interrupt\n%% ");
}

注意:信号处理函数里应该只能调用可重入函数。printf是不可重入函数,不应这么写,这里只是个demo。

可重入函数:可以在它还没有返回就再次被调用的函数。

不可重入函数:不可以在它还没有返回就再次被调用的函数。

1.10 时间值

当度量一个进程的执行时间时,Unix系统为一个进程维护了3个进程时间:

1 时钟时间:是进程运行的时间总量

2 用户CPU时间:执行用户指令所用的时间

3 系统CPU时间:为该程序执行内核程序的时间

例如,测试top命令的进程时间:

time -p top

测试结果:
real 4.39
user 0.00
sys  0.01

1.11 系统调用和库函数

Unix实现提供了直接进入内核的入口点,这些入口点被称为系统调用(system call)。

《Unix程序员手册》的第2部分说明了系统调用接口,第3部分说明了通用库函数。

下图显示了应用程序、malloc函数和sbrk系统调用之间的关系:

可以看到,sbrk系统调用分配了一块内存给进程,并将虚拟地址空间映射到内存供malloc使用,而库函数malloc则在用户层次管理这一内存空间。

系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。如:

1 sbrk与malloc

2 不带缓冲的I/O与标准I/O

3 进程控制系统调用(fork、exec和wait)与system、popen

Guess you like

Origin blog.csdn.net/u012906122/article/details/120067034