0%

以下脚本可以查看echo输出的各种颜色效果:

#!/bin/bash
#
#   This file echoes a bunch of color codes to the
#   terminal to demonstrate what's available.  Each
#   line is the color code of one forground color,
#   out of 17 (default + 16 escapes), followed by a
#   test use of that color on all nine background
#   colors (default + 8 escapes).
#

T='gYw'   # The test text

echo -e "\n                 40m     41m     42m     43m\
    44m     45m     46m     47m";

for FGs in '    m' '   1m' '  30m' '1;30m' '  31m' '1;31m' '  32m' \
    '1;32m' '  33m' '1;33m' '  34m' '1;34m' '  35m' '1;35m' \
    '  36m' '1;36m' '  37m' '1;37m';
do FG=${FGs// /}
    echo -en " $FGs \033[$FG  $T  "
    for BG in 40m 41m 42m 43m 44m 45m 46m 47m;
    do echo -en "$EINS \033[$FG\033[$BG  $T  \033[0m";
    done
    echo;
done
echo

代码如下,实现RGB颜色空间的显示:

for(r = 0 ; r < 0xFF; r++)
{   
    for(g = 0 ; g < 0xFF; g++)
    {   
        for(b = 0 ; b < 0xFF; b++)                                                                                         
        {   
            count++;

            printf("\e]4;60;rgb:%d/%d/%d\e\\\e[38;5;60m 0x%02x%02x%02x██████\e[m", r, g, b, r, g, b); 

            if(count%8 == 0)
                printf("\n");
        }   
    }   
}   

shell中如下进行显示:

$ echo -en '\e]4;60;rgb:ff/ff/ff\e\\\e[38;5;60m = ██ \e[m\n'

可以方便进行RGB颜色的查询

如下代码实现YCbCr(6:4:4:2)到RGB(5:6:6)的转换:

for(i = 0 ; i < 0x10000; i++)
{   
    count++;

    //得到y,cb,cr颜色分量
    y = (i&0xFC00)>>10;
    u = (i&0x03C0)>>6;
    v = (i&0x003C)>>2;

    //每个分量为8bits
    Y = (double) (y<<2);
    Cb = (double) (u<<4);
    Cr = (double) (v<<4);

    //计算r,g,b颜色分量,每个分量为8bits
    r = (int) (Y + 1.40200 * (Cr - 0x80));
    g = (int) (Y - 0.34414 * (Cb - 0x80) - 0.71414 * (Cr - 0x80));
    b = (int) (Y + 1.77200 * (Cb - 0x80));

    r = max(0, min(255, r));
    g = max(0, min(255, g));
    b = max(0, min(255, b));

    //r = (unsigned char)(1.164383*(y- 16) + 0 + 1.596027*(v - 128)); 
    //g = (unsigned char)(1.164383*(y- 16) - 0.391762*(u - 128) - 0.812969*(v - 128)); 
    //b = (unsigned char)(1.164383*(y- 16) + 2.017230*(u - 128) + 0 );

    //printf("\033[32m y : [0x%04x] %x u : %x v : %x \t RGB:[0x%02x%02x%02x] \n\033[0m", i, y, u ,v ,r, g, b);
    //printf("%x %x %x\n", (((unsigned int) r)&0x00F8)<<8, (((unsigned int) g)&0x00FC)<<3, (((unsigned int) b)&0x00F8)>>3);

    //计算5:6:5 RGB
    rgb = ((((unsigned int) r)&0x00F8)<<8) + ((((unsigned int) g)&0x00FC)<<3) + ((((unsigned int) b)&0x00F8)>>3);

    //printf("565RGB : %x", rgb);

    printf("0x%04x, ", rgb);
    if(count%8 == 0)
        printf("\n");
}
  1. Yuv-Wiki
  2. RGB与YCbCr颜色空间的转换
  3. RGB与YCbCr颜色空间的转换
  4. Color Conversion
  5. Function to convert YCbCr to RGB?


专注于用户空间的系统级编程–即内核之上的所有内容。

UNIX系统把绝对时间表示成新纪元至今所经过的秒数,新纪元定义成1970年1月1日早上00:00:00 UTC。
UTC(协调世界时,Coordinated Universal Time)相当于GMT或Zulu时间。

时间的数据结构

原始表示:

#incliude <time.h>
typedef long time_t;

微秒级精度

#include <sys/time.h>

struct timeval {
    time_t      tv_sec;
    suseconds_t tv_usec;
};

纳秒级精度

#include <time.h>

struct timespec {
    time_t  tv_sec;
    long    tv_nsec;
};

时间描述

#include <time.h>

struct tm
{
  int tm_sec;           /* Seconds. [0-60] (1 leap second) */
  int tm_min;           /* Minutes. [0-59] */
  int tm_hour;          /* Hours.   [0-23] */
  int tm_mday;          /* Day.     [1-31] */
  int tm_mon;           /* Month.   [0-11] */
  int tm_year;          /* Year - 1900.  */
  int tm_wday;          /* Day of week. [0-6] */
  int tm_yday;          /* Days in year.[0-365] */
  int tm_isdst;         /* DST.     [-1/0/1]*/

# ifdef __USE_BSD
  long int tm_gmtoff;       /* Seconds east of UTC.  */
  const char *tm_zone;      /* Timezone abbreviation.  */
# else
  long int __tm_gmtoff;     /* Seconds east of UTC.  */
  const char *__tm_zone;    /* Timezone abbreviation.  */
# endif
};

时间API

在Linux上,所有使用POSIX时钟的函数都需要将目标文件与librt链接:

$ gcc -Wall -W -O2 -lrt -g -o a a.c

取得、设置目前时间:

#include <time.h>

time_t time(time_t *t);
int stime(time_t *t);

time()如果参数t非NULL,也将当前时间写入到提供的指针t中。


对某些需要较高精准度的需求,Linux提供了以下接口,提供微秒级精度支持:

#include <sys/time.h>

int gettimeofday(struct timeval * tv, struct timezone *tz);  
int settimeofday(const struct timeval * tv, const struct timezone *tz);

当前时间tv指向timeval结构体。结构提timezone和参数tz已经废止,都不应该在Linux中使用,建议传NULL。


用于纳秒级的高级接口:

#include <time.h>

int clock_gettime(clockid_t clock_id, struct timespec *ts);
int clock_settime(clockid_t clock_id, const struct timespec *ts);

文字时间格式函数:

#include <time.h>

//将tm转换为ASCII字符串
char * asctime(const struct tm *tm);
char * asctime_r(const struct tm *tm, char *buf);

//将time_t转换为ASCII字符串
char * ctime(const struct time_t *tm);
char * ctime_r(const struct time_t *tm, char *buf);

//将tm转换为time_t,使用本地时间
time_t mktime(struct tm *tm);

//将time_t转换为tm,使用UTC时区格式
struct tm * gmtime(const time_t *tp);
struct tm * gmtime_r(const time_t *tp, struct tm *result);

//将time_t转换为tm,使用本地时区格式
struct tm * localtime(const time_t *tp);
struct tm * localtime_r(const time_t *tp, struct tm *result);

//计算秒差
double difftime(time_t time1, time_t time2);

后缀_r的函数不使用静态分配的指针,而是通过参数传递。


睡眠函数:

#include <unistd.h>

unsigned int sleep(unsigned int seconds);   //秒级
void usleep(unsigned long usec);            //微秒级

纳秒级精度睡眠

#include <time.h>

int nanosleep(const struct timespec *req, struct timrspec *rem);

这个函数功能是暂停某个线程直到你规定的时间后恢复,参数req就是你要暂停的时间。
由于调用nanosleep是使线程进入TASK_INTERRUPTIBLE,这种状态是会相应信号而进入TASK_RUNNING状态的,
这就意味着有可能会没有等到你规定的时间就因为其它信号而唤醒,
此时函数返回-1,且还剩余的时间会被记录在rem中(rem不为空的情况下)。


实现睡眠的高级方法??

#include <time.h>  

int clock_nanosleep(clockid_t clock_id, int flags,  
                    const struct timespec *request,  
                    struct timespec *remain);  

select()实现sleep可移植实现:

struct timeval tv = {.tv_sec = 0, .tv_usec = 750};

/* sleep for 750 us */
select(0, NULL, NULL, NULL, &tv);

select的精确度为10毫秒,在10毫秒以上很精确,sleep 可以在多线程中使用,只阻塞本线程,不影响所属进程中的其它线程。
Linux下短延时推荐使用select函数。

定时器

简单定时器接口:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

想要成功调用该函数,需要为SIGALRM信号注册一个信号处理函数:

signal(SIGALRM, alarm_handler);

计时器

#include <sys/time.h>

int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

高级定时器

#include <signal.h>
#include <time.h>

int timer_create(clockid_t clockid, struct sigevent *evp, timer_t *timerid);
int timer_settime(timer_t timerid, int flags, const struct itimerspec *value, struct itimerspec *ovalue);
int timer_gettime(timer_t timerid, struct itimerspec *value);
int timer_getoverrun(timer_t timerid);
int timer_delete(timer_t timerid);

*关于短延迟 sleep usleep nanosleep select


专注于用户空间的系统级编程–即内核之上的所有内容。

概念

信号是一种软件中断,它提供了异步时间的处理机制。
信号作为一种进程间通信(IPC)的基本形式,而一个进程可以给另一个进程发送信号。

信号有非常明显的生命周期。首先,产生信号(信号被发出或生成)。
然后内核存储信号,至到可以发送该信号。
最后,一旦空闲,内核就会适当地处理信号。
根据进程的请求,内核会执行以下三种操作之一:

  • 忽略信号。SIGKILL和SIGSTOP不能忽略
  • 捕获并处理信号。暂停该进程正在执行的代码,并跳转到先前注册的函数。返回后跳回捕获信号的地方继续执行
  • 执行信号的默认操作。

信号标识符以SIG作为前缀的正整数,在头文件<signal.h>中定义,可以使用以下命令查看系统提供的信号列表:

$ kill -l

信号的分类及生命周期

  • 非可靠信号:早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
  • 可靠信号: 信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。
当然也可以称为实时信号或者非实时信号,非实时信号都不支持排队,都是不可靠信号;
实时信号都支持排队,都是可靠信号。

信号的生命周期:
从信号发送到信号处理函数的执行完毕。
对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,
这三个阶段由四个重要事件来刻画:

  • 信号产生
  • 信号在进程中注册完毕
  • 信号在进程中的注销完毕
  • 信号处理函数执行完毕。

相邻两个事件的时间间隔构成信号生命周期的一个阶段。

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,
信号不会丢失,因此,实时信号又叫做”可靠信号”。
这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,
并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。
因此,非实时信号又叫做”不可靠信号”。这意味着同一个非实时信号在进程的未决信号信息链中,
至多占有一个sigqueue结构(一个非实时信号产生后,1如果发现相同的信号已经在目标结构中注册,则不再注册,
对于进程来说,相当于不知道本次信号发生,信号丢失;2如果进程的未决信号中没有相同信号,则在进程中注册自己)。

需要注意的要点是:

  • 信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,
    只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。
  • 在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,
    每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。

信号API

发送信号函数

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int signo);

kill()系统调用,用于向另外一个进程发送信号。
参数pid的值为信号的接收进程:

  • pid>0 进程ID为pid的进程
  • pid=0 同一个进程组的进程
  • pid<0 pid!=-1 进程组ID为 -pid的所有进程
  • pid=-1 除发送进程自身外,所有进程ID大于1的进程

signo是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,
以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,
非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

#include <signal.h>

int raise(int signo);

raise()用于给进程本身发送信号。

#include <sys/types.h>
#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val);

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,
与函数sigaction()配合使用。
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,
指定了信号传递的参数,即通常所说的4字节值。

typedef union sigval 
{
    int sival_int;
    void *sival_ptr;
}sigval_t;

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果sig为0,
将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指的是信号处理函数由sigaction安装,
并设定了sa_sigaction指针,稍后将阐述)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用
支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数;
sigqueue()发送非实时信号时,仍然不支持排队,
即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm()专门为SIGALRM信号而设,在指定的时间seconds秒后,
将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,
任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。
返回值:如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

#include <sys/time.h>

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));

setitimer()比alarm功能强大,支持3种类型的定时器:

  • ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程
  • ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程
  • ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程

#include <stdlib.h>

void abort(void);

abort()向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。
即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

信号的捕获与安装

如果进程要处理某一信号,那么就要在进程中安装该信号。
安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,
即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。
linux主要有两个函数实现信号的安装:signal()、sigaction()。
其中signal()在可靠信号系统调用的基础上实现, 是库函数。
它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;
而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),
有三个参数,支持信号传递信息,主要用来与sigqueue()系统调用配合使用,
当然,sigaction()同样支持非实时信号的安装。
sigaction()优于signal()主要体现在支持信号带有参数。

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理:

  • SIG_IGN,忽略该信号
  • SIG_DFL,系统默认方式处理信号
  • 也可以自己实现处理方式(参数指定一个函数地址)

第三种用法如下:

static void sigint_handler(int signo)
{
    printf("Caught SIGINT!\n");
    exit(EXIT_SUCCESS);
}

int main(void)
{
    if(signal(SIGIN, sigint_handler) == SIG_ERR) {
        fprintf(stderr, "Cannot handle SIGINT!\n");
        exit(EXIT_FAILURE);
    }

    for(;;)
        pause();

    return 0;
}

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。
该函数的第一个参数为信号的值,
可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号
(为这两个信号定义自己的处理函数,将导致信号安装错误)。
第二个参数是指向结构sigaction的一个实例的指针,
在结构sigaction的实例中,指定了对特定信号的处理,
可以为空,进程会以缺省方式对信号处理;
第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。
如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。
sigaction结构定义如下:

struct sigaction {
    void (*sa_handler)(int signo);
    void (*sa_sigaction)(int siginfo_t *info,void *act);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restore)(void);
}

sa_handler以及sa_sigaction指定信号关联函数,即用户指定的信号处理函数。
除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。
sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息。

如果sa_flags被设置为SA_SIGINFO,那么将由sa_sigaction,而不是sa_handler来设置信号关联函数。
sa_sigaction信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。

第一个参数为信号值,第三个参数没有使用(POSIX没有规范使用该参数的标准),
第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

siginfo_t {
    int si_signo; /* 信号值,对所有信号有意义*/
    int si_errno; /* errno值,对所有信号有意义*/
    int si_code; /* 信号产生的原因,对所有信号有意义*/
    union{ /* 联合数据结构,不同成员适应不同信号 */ 
        //确保分配足够大的存储空间
        int _pad[SI_PAD_SIZE];
    //对SIGKILL有意义的结构
    struct{
        ...
    }...

    ... ...
    ... ... 
    //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
    struct{
        ...
    }...
    ... ...
}

}

注:为了更便于阅读,在说明问题时常把该结构表示为如下所表示的形式。

siginfo_t {
    int si_signo;       /* 信号值,对所有信号有意义*/
    int si_errno;       /* errno值,对所有信号有意义*/
    int si_code;        /* 信号产生的原因,对所有信号有意义*/
    pid_t si_pid;       /* 发送信号的进程ID,对kill(2),实时信号以及SIGCHLD有意义 */
    uid_t si_uid;       /* 发送信号进程的真实用户ID,对kill(2),实时信号以及SIGCHLD有意义 */
    int si_status;      /* 退出状态,对SIGCHLD有意义*/
    clock_t si_utime;   /* 用户消耗的时间,对SIGCHLD有意义 */
    clock_t si_stime;   /* 内核消耗的时间,对SIGCHLD有意义 */
    sigval_t si_value;  /* 信号值,对所有实时有意义,是一个联合数据结构,见函数sigqueue()*/
    void * si_addr;     /* 触发fault的内存地址,对SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义*/
    int si_band;        /* 对SIGPOLL信号有意义 */
    int si_fd;          /* 对SIGPOLL信号有意义 */
} 

实际上,除了前三个元素外,其他元素组织在一个联合结构中,在联合数据结构中,又根据不同的信号组织成不同的结构。
注释中提到的对某种信号有意义指的是,
在该信号的处理函数中可以访问这些域来获得与信号相关的有意义的信息,只不过特定信号只对特定信息感兴趣而已。

调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,
当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。
这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。
信号参数的传递过程:

sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。
缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。
请注意sa_mask指定的信号阻塞的前提条件:是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。


专注于用户空间的系统级编程–即内核之上的所有内容。

内存管理包括内存分配(allocation)、内存操作(manipulation)和内存释放(release)。管理内存相关的函数如下:

linux下的管理内存相关的函数

进程地址空间

进程地址空间由进程可寻址的虚拟内存组成,对于某个虚拟内存地址,
它要在地址空间范围内,例如: 0421f000,这个值表示的是进程32位地址空间中的一个特定的字节。
尽管一个进程可以寻址4GB的虚拟内存(在32位的地址空间中),
但是这并不代表它有权访问所有的虚拟地址。
在地址空间中,我们更常用或者关心的是某个虚拟内存地址空间,比如 0848000-084c000,
它们可以被进程访问。我们称这些可被访问的合法地址空间称为虚拟内存区域。
通过内核,进程可以给自己的地址空间动态的增加或减少虚拟内存区域。

内核将具有某些相同特征的页组织成块(blocks),例如读写权限。
这些块叫做内存区域(memory regions),段(segments),或者映射(mappings)。
Linux进程的虚拟内存区域一般有:

  • 文本段(text segment)包含一个进程的代码、字符串、常量和一些只读数据。Linux直接从目标文件映射到内存中
  • 堆栈段(stack)包含一个进程的执行栈,随着栈的深度变化会自动伸缩。执行栈中包含局部变量和函数的返回值
  • 数据段(data segment),又叫堆(heap),包含一个进程的动态内存空间,通常由malloc()来分配
  • BSS段(bss segment)包含了未被初始化的全局变量

每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程访问。
内核不用记录那些不存在的虚拟页,而这样的页也不占用存储器、磁盘或者内核本身的其他任何资源。

进程只能访问有效内存区域的内存地址,每个内存区域也具有相关权限,如可读、可写、可执行性质。
如果一个进程访问了无效范围中的内存区域或者以不正确的方式访问了有效地址,
那么内核就会终止该进程,并返回 “段错误”信息。

动态内存分配

C提供用于获取动态内存的接口:

#include <stdlib.h>

void * malloc(size_t size);
void * calloc(size_t nr, size_t size);
void * realloc(void *ptr, size_t size);
void free(void *ptr);

malloc()用于申请一段新的地址,参数size为需要内存空间的长度。可能返回NULL,必须检查返回值有效性。

calloc()与malloc()相似,参数size为申请地址的单位元素长度,nr为元素个数,常用于数组分配
和malloc()不同的是,calloc()将分配的区域全部用0进行初始化。

realloc()是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,size是重新申请的地址长度。
size为0,相当于调用free();ptr为NULL,相当于调用malloc()。

free()参数ptr可能是NULL,这时free()不做任何事情就返回,因此调用free()不需要检查ptr是否为NULL。

realloc()和free()的ptr参数必须是通过调用malloc()、calloc()或者realloc()的返回值。

  • malloc与calloc的区别为1块与n块的区别:
  • malloc调用形式为(类型*)malloc(size):在内存的动态存储区中分配一块长度为“size”字节的连续区域,返回该区域的首地址。
  • calloc调用形式为(类型*)calloc(n,size):在内存的动态存储区中分配n块长度为“size”字节的连续区域,返回首地址。

对齐

数据的对齐(alignment)是指数据的地址和由硬件条件决定的内存块大小之间的关系。
一个变量的地址是它大小的倍数的时候,这就叫做自然对齐 (naturally aligned)。
例如,对于一个32bit的变量,如果它的地址是4的倍数,
就是说,如果地址的低两位是0,那么这就是自然对齐了。
所以,如果一个类型的大小是2n个字节,那么它的地址中,至少低n位是0。
对齐的规则是由硬件引起的。
一些体系的计算机在数据对齐这方面有着很严格的要求。
在一些系统上,一个不对齐的数据的载入可能会引起进程的陷入。
在另外一些系统,对不对齐的数据的访问是安全的,但却会引起性能的下降。
在编写可移植的代码的时候,对齐的问题是必须避免的,所有的类型都该自然对齐。

在大多数情况下,编译器和C库透明地帮你处理对齐问题。
POSIX 标明了通过malloc(), calloc(), 和realloc()返回的地址对于任何的C类型来说都是对齐的。
在Linux中,这些函数返回的地址在32位系统是以8字节为边界对齐,
在64位系统是以16字节为边界对齐 的。有时候,对于更大的边界,例如页面,程序员需要动态的对齐。
虽然动机是多种多样的,但最常见的是直接块I/O的缓存的对齐或者其它的软件对硬件的交互,
因此,POSIX 1003.1d提供一个叫做posix_memalign( )的函数:

/* one or the other -- either suffices */
#define _XOPEN_SOURCE 600
#define _GNU_SOURCE

#include <stdlib.h>

int posix_memalign (void **memptr,
        size_t alignment,
        size_t size);

调用posix_memalign()成功时会返回size字节的动态内存,
并且这块内存的地址是alignment的倍数。
参数alignment必须是2的幂,还是void指针的大小的倍数。
返回的内存块的地址放在了memptr里面,函数返回值是0。

调用失败时,没有内存会被分配,memptr的值没有被定义,返回错误码。
要注意的是,对于这个函数,errno不会被设置,只能通过返回值得到。

由posix_memalign()获得的内存通过free()释放。用法很简单:

char *buf;
int ret;

/* allocate 1 KB along a 256-byte boundary */
ret = posix_memalign (&buf, 256, 1024);
if (ret) {
    fprintf (stderr, "posix_memalign: %s\n",
            strerror (ret));
    return -1;
}
/* use 'buf'... */
free (buf);

更早的接口。在POSIX定义了posix_memalign( )之前,BSD和SunOS分别提供了如下接口:

#include <malloc.h>
void * valloc (size_t size);
void * memalign (size_t boundary, size_t size);

函数valloc()的功能和malloc()一模一样,但返回的地址是页面对齐的。
页面的大小很容易通过getpagesize()得到。

相似地,函数memalign()是以boundary字节对齐的,而boundary必须是2的幂。
在这个例子中,两个函数都返回一块足够大的内存去容纳一个ship结构,并且地址都是在一个页面的边界上:

struct ship *pirate, *hms;
pirate = valloc (sizeof (struct ship));
if (!pirate) {
    perror ("valloc");
    return -1;
}
hms = memalign (getpagesize ( ), sizeof (struct ship));
if (!hms) {
    perror ("memalign");
    free (pirate);
    return -1;
}
/* use 'pirate' and 'hms'... */
free (hms);
free (pirate);

在 Linux中,由这两个函数获得的内存都可以通过free()释放。
但在别的Unix系统却未必是这样,
一些系统并没有提供一个足够安全的机制去释放这些内存。
考虑移植性的程序不得不放弃使用这些接口来获得动态内存。
Linux程序员最好只在考虑对老系统的兼容性时才使用它们;posix_memalign()更加强大。
只有在malloc()不能提供足够大的对齐时,这三个接口才需要使用。

数据段的管理

Unix系统在历史上提供过直接管理数据段的接口。
然而,程序都没有直接地使用这些接口,因为malloc()和其它的申请方法更容易使用和更加强大。
在这里说一下这些接口来满足一下大家的好奇心,同时也给那些想实现他自己的基于堆栈的动态内存申请机制的人一个参考:

#include <unistd.h>

int brk (void *end);
void * sbrk (intptr_t increment);

这些功能的名字源于老版本的Unix系统,那时堆和栈还在同一个段中。
堆中动态存储器的分配由数据段的底部向上生长;栈从数据段的顶部向着堆向下生长。
堆和栈的分界线叫做break或break point。
在现代的系统里面,数据段存在于它自己的内存映射,我们继续用断点来标记映射的结束地址。

一个brk()的调用设置断点(数据段的末端)的地址为end。在成功的时候,返回0。失败的时候,返回-1,并设置errno为ENOMEM。

一个sbrk()的调用将数据段末端生长increment字节,increment可能是正数,也可能是负数。
sbrk()返回修改后的断点。所以,increment为0时得到的是现在断点的地址:

printf ("The current break point is %p\n", sbrk (0));

尽管POSIX和C都没有定义这些函数。但几乎所有的Unix系统,都提供其中一个或全部。
可移植的程序应该坚持使用基于标准的接口。

匿名内存映射

glibc的动态存储器使用了数据段和内存映射。
实现malloc()的经典方法是将数据段分为一系列的大小为2的幂的分区,
返回最小的符合要求的那个块来满足请求。
释放内存只是简单地将这块区域标记为“未使用”。
如果临近的分区是空闲的,他们会被合成一个更大的分区。
如果断点的下面是空的,系统可以用brk()来降低断点,使堆收缩,将内存返回给系统。

这个算法叫做伙伴内存分配算法(buddy memory allocation scheme)
它的优势是高速和简单,但不好的地方是引入了两种碎片。
内部碎片(Internal fragmentation)发生在用更大的块来满足一个分配。
这样导致了内存的低使用率。
当有着足够的空闲内存来满足要求但这“块”内存分布在两个不相邻空间的时候,
外部碎片(External fragmentation)就产生了。
这会导致内存的低使用率(因为一块更大的不够适合的块可能被使用了),或者内存分配失败(在没有可供选择的块时)。

更有甚者,这个算法允许一个内存的分配“栓”住另外一个,
使得glibc不能向内核归还内存。
想象内存中的已被分配的两个块,块A和块B。
块A刚好在断点的下面,块B刚好在A的下面,就算释放了B,glibc也不能相应的调整断点直到A被释放。
在这种情况,一个长期存在的内存分配就把另外的空闲空间“栓”住了。

但这不需太过担忧。因为glibc并没有严格地将将释放的空闲内存返回给系统。
通常来说,在每次释放后堆并不收缩。
相反,glibc会维护释放的内存,为后续的分配保留着些自由的空间。
只有在堆与已分配的空间相比明显太大的时候,
glibc才会把堆缩小。然而,一个更大的分配,就能防止这个收缩了。

因此,对于较大的分配,glibc并不使用堆。
glibc使用一个匿名存储器映射(anonymous memory mapping)来满足请求。
匿名存储器映射和基于文件的映射是相似的,只是它并不基于文件-所以称为之“匿名”。
实际上,匿名存储器映射是一个简单的全0填充的大内存块,以供用户使用。
因为这种映射的存储不是基于堆的,所以并不会在数据段内产生碎片。

通过匿名映射来分配内存又下列好处:

  • 无需关心碎片。当程序不再需要这块内存的时候,只是撤销映射,这块内存就直接归还给系统了
  • 匿名存储器映射能改变大小,有着改变大小的能力,还能像普通的映射一样接收参数
  • 每个分配存在于独立的内存映射。没有必要再去管理一个全局的堆

下面是两个使用匿名存储器映射而不使用堆的劣处:

  • 每个存储器映射都是页面大小的整数倍。
    所以,如果大小不是页面整数倍的分配会浪费大量的空间。
    这些空间更值得忧虑,因为相对于被分配的空间,被浪费掉的空间往往更多。
  • 建立一个存储器映射比将堆里面的空间回收利用的代价更大,
    因为堆几乎不涉及任何内核操作。越小的分配,这个劣处就明显。

权衡优缺点,glibc的malloc() 用数据段来满足小的分配,用存储器映射来满足大的分配。
临界点是可被设定的(看后面的高级内存分配),并且随着glibc版本的不同而变化。
目前,临界点一般是128KB:比128KB小的分配由堆实现,相应地,更大的由匿名存储器映射来实现。

创建匿名存储器映射

可能你会想强制在堆上使用存储器映射来满足一个特定的内存分配,
也可能你会想写一个自己的存储分配系统,总之你可能会要手动创建你自己的匿名内存映射,Linux让这变得很简单。
回想第四章系统调用,用来创建存储器映射的mmap( )和取消映射的munmap( ):

#include <sys/mman.h>

void * mmap (void *start,
        size_t length,
        int prot,
        int flags,
        int fd,
        off_t offset);

int munmap (void *start, size_t length);

因为没有文件需要打开和管理,创建匿名存储器映射真的要比创建基于文件的存储器映射简单。
两者最关键的差别在于匿名标记是否出现,表示该映射是匿名映射。让我们来看看这个例子:

void *p;

p = mmap (NULL,                         /* do not care where */
        512 * 1024,                     /* 512 KB */
        PROT_READ | PROT_WRITE,         /* read/write */
        MAP_ANONYMOUS | MAP_PRIVATE,    /* anonymous, private */
        -1,                             /* fd (ignored) */
        0);                             /* offset (ignored) */

if (p == MAP_FAILED)
    perror ("mmap");
    else
    /* 'p' points at 512 KB of anonymous memory... */

对于大多数的匿名映射来说,mmap( )的参数都跟这个例子一样,
当然了,程序员决定的映射大小这个参数是个例外。别的参数一般都像这样:

  • 第一个参数是start,被设为NULL,意味着匿名映射可以在内核安排的任意地址上发生。
    当然给定一个non-NULL值也是有可能的,必须是页对齐的,但这样会限制了可移植性。
    实际上很少有程序真正在意映射到哪个地址上
  • prot参数经常都同时设置了PROT_READ和PROT_WRITE位,使得映射是可读可写的。
    一块不能读写的空存储器映射是没有用的。
    另外一方面,很少将可执行代码映射到匿名映射,因为那样做能产生潜在的安全漏洞。
  • flags参数设置MAP_ANONYMOUS位,来使得映射是匿名的,设置MAP_PRIVATE位,使得映射是私有的。
  • 假如MAP_ANONYMOUS被设置了,fd和offset参数将被忽略的。
    然而,在一些更早的系统里,需要让fd为-1,如果要考虑移植性,需要设置为-1。

由匿名映射获得的内存块,看上去和由堆获得的一样。
使用匿名映射的一个好处是,那块内存交给你的时候,已经是全0的了。
这种映射还没有额外的开销,因为内核使用写时复制(copy-on-write)将内存块映射到了一个全0的页面上。
所以没有必要对返回的内存块使用memset()。
事实上,这是使用calloc()比使用malloc()后再使用memset()效率更高的原因之一。

系统调用munmap()释放一个匿名映射,归还已分配的内存给内核。

int ret;

/* all done with 'p', so give back the 512 KB mapping */
ret = munmap (p, 512 * 1024);
if (ret)
    perror ("munmap");

映射到/dev/zero

其它Unix系统,就像BSD,并没有MAP_ANONYMOUS标记。
作为替代,它们用一个特殊的设备文件/dev/zero实现了一个类似的解决方法。
这个设备文件提供了和匿名存储器语义上一致的实现。
一个映射包含了全0的写时复制页面;所以行为上和匿名存储器一样。

Linux一直支持/dev/zero设备,可以由映射这个文件来获得全0的内存块。
实际上,在引入之前MAP_ANONYMOUS,Linux的程序员就是这样做的。
为了对早期的Linux版本提供向后兼容性,或者对其他Unix系统的可移植性,
程序员仍然可以将映射/dev/zero作为匿名映射的替代。

void *p;
int fd;

/* open /dev/zero for reading and writing */
fd = open ("/dev/zero", O_RDWR);
if (fd < 0) {
    perror ("open");
    return -1;
}

/* map [0,page size) of /dev/zero */
p = mmap (NULL,                 /* do not care where */
        getpagesize ( ),        /* map one page */
        PROT_READ | PROT_WRITE, /* map read/write */
        MAP_PRIVATE,            /* private mapping */
        fd,                     /* map /dev/zero */
        0);                     /* no offset */

if (p == MAP_FAILED) {
    perror ("mmap");
    if (close (fd))
        perror ("close");
    return -1;
}

/* close /dev/zero, no longer needed */
if (close (fd))
    perror ("close");

/* 'p' points at one page of memory, use it... */

在这种情况下映射的存储器也是用munmap()来取消映射。
这种实现引入了附加的打开和关闭文件的系统调用,所以会有额外的系统调用开销。
相比之下,匿名内存映射是一种较快的方法。

高级内存分配

很多内存分配操作受到glibc或内核的参数所限制和控制,可以通过调用mallopt()函数修改这些参数:

#include <malloc.h>

int mallopt(int param, int value);

控制内存分配函数的行为,param表示参数,value表示值。Linux目前支持6种param值:

  • M_CHECK_ACTION,环境变量MALLOC_CHECK_的值
  • M_MMAP_MAX,最大内存映射数。达到限制后只能在数据段中分配,当为0时禁止使用匿名映射分配内存
  • M_MMAP_THRESHOLD,匿名映射阀值,当值为0时,所有分配启用匿名映射
  • M_MXFAST,fast bin的最大大小,fast bins是堆最特殊的内存块,永远不和临近的内存合并。当值为0时,禁止fast bin
  • M_TOP_PAD,当在堆顶的空闲空间到达设定的值时,则将这些内存还给系统。
    这样做的目的是在堆项保留一些空闲空间,防止频繁的申请内存的系统调用
  • M_TRIM_THRESHOLD,堆的最小保持空间,只有超过这个设定值的堆内存,才有可能被sbrk回收,返还给系统

程序必须在调用malloc()或其他内存分配之前,使用mallopt(),使用方法如下:

int ret;

ret = mallopt(M_MMAP_THRESHOLD, 64*1024);
if(!ret)
    perror("mallopt");

Linux提供了一组用来控制glibc内存分配系统的底层函数:

#include <malloc.h>

size_t malloc_usable_size(void *ptr);   //查询一块已分配内存中有多少可用字节
int malloc_trim(size_t padding);        //强制glibc归还所有可释放的动态内存给内核,保留填充字节

除了调试和教学之外,其他地方几乎永远都不要使用这两个函数。
他们是不可移植的,而且会将glibc内存分配系统的一些底层细节暴露给应用程序。

调试内存分配,设置环境变量MALLOC_CHECK_,开启储存系统中高级的调试功能。
不必重新编译程序,简单执行如下命令:

$ MALLOC_CHECK_=1 ./out.elf
  • 0:忽略所有错误
  • 1:信息输出到标准错误输出stderr
  • 2:立即通过abort()终止

获取统计信息,通过mallinfo()函数,可以获取动态内存分配系统相关的统计信息,
malloc_stats()函数可以将内存相关的统计信息打印到stderr:

#include <malloc.h>

struct mallinfo                                                                                                            
{
    int arena;    /* non-mmapped space allocated from system */
    int ordblks;  /* number of free chunks */
    int smblks;   /* number of fastbin blocks */
    int hblks;    /* number of mmapped regions */
    int hblkhd;   /* space in mmapped regions */
    int usmblks;  /* maximum total allocated space */
    int fsmblks;  /* space available in freed fastbin blocks */
    int uordblks; /* total allocated space */
    int fordblks; /* total free space */
    int keepcost; /* top-most, releasable (via malloc_trim) space */
};

struct mallinfo(void);
void malloc_stats(void);

基于栈的分配

alloca()函数,可以在堆栈上分配一块内存,当前函数退出时,由于系统堆栈指针的调整,这块内存会被自动回收。

#include <alloca.h>

void * alloca(size_t size);

当调用alloca()的函数返回后,就不能再使用由alloca()得到的那块内存!
更要注意的是不要使用alloca()函数分配的内存作为一个函数的调用参数,
因为分配的内存块会存在于函数参数所保存的栈空间中。

在各种编程文档中已经不太提倡使用了。因为它有许多不安全因素。
如果希望代码具有可移植性,应该避免使用alloca()函数。
但是,在Linux系统上,alloca()是一个非常好用的工具,表现很出色(使用calloca()分配内存和增加栈指针一样简单)。
在Linux下,对较小内存的分配,比malloc()有很大的性能提升。

内存操作

主要包含字节设置、字节比较、字节移动、字节查找和字节加密函数:

#include <string.h>

void * memset(void *s, int c, size_t n);
int memcmp(const void *s1, const void *s2, size_t n);
void memmove(void *dst, const void *src, size_t n);     //支持内存区域重叠
void memcpy(void *dst, const void *src, size_t n);      //不支持内存区域重叠,但是可能效率高一些
void memccpy(void *dst, const void *src, int c, size_t n);
void memchr(const char *s, int c, size_t n);
void memrchr(const char *s, int c, size_t n);
void * memfrob(void *s, size_t n);      //加密,第二次调用解密

内存锁定

用于将一个或多个页面“锁定”在物理内存,以保证他们不会被交换到磁盘:

#include <sys/mman.h>

int mlock(const void *addr, size_t n);
int mlockall(int flags);
int munlock(const void *addr, size_t n);
int munlockall(void);

投机性内存分配策略

Linux使用一种“投机性内存分配策略(opportunistic allocation strategy)”。
当进程想内核请求额外的内存时,内核作出了承诺,但实际上没有分配给进程任何物理存储。
仅当进程对新“分配到”的内存区域执行读写操作时,内核才履行承诺,分配一块物理内存。

出于以上策略,分配到的内存可能比实际的物理内存甚至比交换空间多得多。
这个特征叫做“超量使用(overcommitment)”。

当超量使用导致内存不足以满足一个请求时,就发生了“内存溢出(out of memory)”。
为了处理OOM,内核使用OOM Killer来挑选一个进程(最不重要又占用很多内存),并终止它。

可以通过修改/proc/sys/vm/overcommit_memory来控制超量使用:

  • 0:默认值,适度的超量使用策略
  • 1:没有顾忌
  • 2:禁止使用


专注于用户空间的系统级编程–即内核之上的所有内容。

与互斥锁不同,条件变量是用来等待而不是用来上锁的。
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量使我们可以睡眠等待某种条件出现。
条件变量是利用线程间共享的全局变量进行同步的一种机制,
主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。

条件的检测是在互斥锁的保护下进行的。
如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。

如果另一个线程改变了条件,它发信号给关联的条件变量,
唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。
如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

使用条件变量之前要先进行初始化。
可以在单个语句中生成和初始化一个条件变量如:pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER;(用于进程间线程的通信)。
可以利用函数pthread_cond_init动态初始化。

条件变量分为两部分: 条件和变量。
条件本身是由互斥量保护的。
线程在改变条件状态前先要锁住互斥量。
它利用线程间共享的全局变量进行同步的一种机制。

API

int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);     
int pthread_cond_destroy(pthread_cond_t *cond);  
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);  //解除所有线程的阻塞

条件变量采用的数据类型是pthread_cond_t, 在使用之前必须要进行初始化, 这包括两种方式:

  • 静态: 可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量
  • 动态: pthread_cond_init函数, 释放动态条件变量的内存空间之前, 要用pthread_cond_destroy对其进行清理

pthread_cond_wait()函数用于等待条件被触发。
该函数传入两个参数,一个条件变量一个互斥量,函数将条件变量和互斥量进行关联,
互斥量对该条件进行保护,传入的互斥量必须是已经锁住的。
调用pthread_cond_wait()函数后,会原子的执行以下两个动作:

  • 将调用线程放到等待条件的线程列表上,即进入睡眠
  • 对互斥量进行解锁

由于这两个操作时原子操作,
这样就关闭了条件检查和线程进入睡眠等待条件改变这两个操作之间的时间通道,
这样就不会错过任何条件的变化。
当pthread_cond_wait()返回后,互斥量会再次被锁住。

pthread_cond_timedwait()函数和pthread_cond_wait()的工作方式相似,只是多了一个等待时间。
等待时间的结构为struct timespec,

struct timespec{  
    time_t  tv_sec    //Seconds.  
    long    tv_nsec   //Nanoseconds.  
};

函数要求传入的时间值是一个绝对值,不是相对值,
例如,想要等待3分钟,必须先获得当前时间,然后加上3分钟。
要想获得当前系统时间的timespec值,没有直接可调用的函数,
需要通过调用gettimeofday函数获取timeval结构,然后转换成timespec结构,转换公式就是:

timeSpec.tv_sec = timeVal.tv_sec;  
timeSpec.tv_nsec = timeVal.tv_usec * 1000; 

所以要等待3分钟,timespec时间结构的获得应该如下所示:

struct timeval now;  
struct timespec until;  
gettimeofday(&now);//获得系统当前时间  

//把时间从timeval结构转换成timespec结构  
until.tv_sec = now.tv_sec;  
until.tv_nsec = now.tv_usec * 1000;  

//增加min  
until.tv_sec += 3 * 60;  

如果时间到后,条件还没有发生,那么会返回ETIMEDOUT错误。

从pthread_cond_wait()和pthread_cond_timewait()成功返回时,
线程需要重新计算条件,因为其他线程可能在运行过程中已经改变条件。

pthread_cond_signal() 和 pthread_cond_broadcast()
这两个函数都是用于向等待条件的线程发送唤醒信号,
pthread_cond_signal()函数只会唤醒等待该条件的某个线程,
pthread_cond_broadcast()会广播条件状态的改变,以唤醒等待该条件的所有线程。
例如多个线程只读共享资源,这是可以将它们都唤醒。
这里要注意的是:一定要在改变条件状态后,再给线程发送信号。
考虑条件变量信号单播发送和广播发送的一种候选方式是坚持使用广播发送。
只有在等待者代码编写确切,只有一个等待者需要唤醒,
且唤醒哪个线程无所谓,那么此时为这种情况使用单播,
所以其他情况下都必须使用广播发送。

在实际编程中,把共享数据和它们的同步变量集合到一个结构中,这往往是一个较好的编程技巧:

struct{  
    pthread_mutex_t mutex;  
    pthread_cond_t cond;  
    int data;  
}Data;  

Source Code

#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/* 初始化互斥锁*/  
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/* 初始化条件变量*/  
void *thread1(void *);  
void *thread2(void *);  
int i=1;  
int main(void)  
{  
    pthread_t t_a;  
    pthread_t t_b;  

    pthread_create(&t_a,NULL,thread1,(void *)NULL);/* 创建进程t_a*/  
    sleep(1);
    pthread_create(&t_b,NULL,thread2,(void *)NULL); /* 创建进程t_b*/  

    pthread_join(t_a, NULL);/* 等待进程t_a结束*/  
    pthread_join(t_b, NULL);/* 等待进程t_b结束*/  

    pthread_mutex_destroy(&mutex);  
    pthread_cond_destroy(&cond);  

    exit(0);  
}  
void *thread1(void *junk)  
{  
    for(i=1;i<=6;i++)  
    {  
        pthread_mutex_lock(&mutex);/* 锁住互斥量*/  
        printf("thread1: lock %d\t i %d\n", __LINE__, i);  

        if(i%3==0){  
            printf("thread1:signal 1  %d\n", __LINE__);  
            pthread_cond_signal(&cond);/* 条件改变,发送信号,通知t_b进程*/  
            printf("thread1:signal 2  %d\n", __LINE__);  
            sleep(1);  
        }  

        pthread_mutex_unlock(&mutex);/* 解锁互斥量*/  
        printf("thread1: unlock %d\t i %d\n\n", __LINE__, i);  
        sleep(1);  
    }  
}  
void *thread2(void *junk)  
{  
    while(i<6)  
    {  
        pthread_mutex_lock(&mutex);  
        printf("thread2: lock %d\t i %d\n", __LINE__, i);  

        if(i%3!=0){  
            printf("thread2: wait 1  %d\n", __LINE__);  
            pthread_cond_wait(&cond,&mutex);/* 解锁mutex,并等待cond改变*/  
            printf("thread2: wait 2  %d\n", __LINE__);  
        }  

        pthread_mutex_unlock(&mutex);  
        printf("thread2: unlock %d\t i %d\n\n", __LINE__, i);  
        sleep(1);  
    }  
}

编译运行结果如下:

thread1: lock 31     i 1
thread1: unlock 41   i 1

thread2: lock 50     i 1
thread2: wait 1  53
thread1: lock 31     i 2
thread1: unlock 41   i 2

thread1: lock 31     i 3
thread1:signal 1  34
thread1:signal 2  36
thread1: unlock 41   i 3

thread2: wait 2  55
thread2: unlock 59   i 3

thread1: lock 31     i 4
thread1: unlock 41   i 4

thread2: lock 50     i 4
thread2: wait 1  53
thread1: lock 31     i 5
thread1: unlock 41   i 5

thread1: lock 31     i 6
thread1:signal 1  34
thread1:signal 2  36
thread1: unlock 41   i 6

thread2: wait 2  55
thread2: unlock 59   i 6

线程1先执行,获得mutex锁,打印,然后释放mutex锁,然后阻塞自己1秒。

线程2执行,阻塞在pthread_mutex_lock(&mutex)
这行语句中,直到线程1释放mutex锁。
然后线程2得已执行,获取metux锁,满足if条件,到pthread_cond_wait (&cond,&mutex)
这里线程2阻塞,不仅仅是等待cond变量发生改变,同时释放mutex锁。
mutex锁释放后,线程1终于获得了mutex锁,得已继续运行,当线程1的if(i%3==0)的条件满足后,
通过pthread_cond_signal发送信号,
告诉等待cond的变量的线程(线程2),cond条件变量已经发生了改变。

线程2并没有立即得到运行,因为线程2还在等待mutex锁的释放,
所以线程1继续往下走,直到线程1释放mutex锁,
线程2才能停止等待,打印语句,
然后往下走通过pthread_mutex_unlock(&mutex)释放mutex锁,进入下一个循环。

References

  • toc
    {:toc}

专注于用户空间的系统级编程–即内核之上的所有内容。

获得文件metadata的Stat家族

#include <sys/types.h>  
#include <sys/stat.h>  
#include <unistd.h>  

struct stat{  
    dev_t st_dev; /*包含文件的设备号*/  
    ino_t st_ino; /*文件inode序列号 */  
    mode_t st_mode;/*文件的mode*/  
    nlink_t st_nlink; /*硬链接的数量*/  
    uid_t st_uid; /*文件的用户id*/  
    gid_t st_gid; /*文件的goup id*/  
    off_t st_size;/*文件大小*/  

    time_t st_atime;/*最后一次访问时间*/  
    time_t st_mtime;/*最后一次修改时间*/  
    time_t st_ctime;/*最后一次状态改变时间*/  
}  

int lstat(const char * restrict path, struct stat * restrict buf);  
int stat(const char *restrict path, struct stat * restrict buf);  
int fstat(int fd,struct stat *buf);  

对于软链来说:

  • lstat返回软链本身的状态
  • stat返回软链所指文件的状态

例子:

#include <stdio.h>  
#include <time.h>  
#include <sys/stat.h>  

int isdirectory(char *path) {  
    struct stat statbuf;  

    if (stat(path, &statbuf) == -1)  
        return 0;  
    else  
        return S_ISDIR(statbuf.st_mode);  
}  

文件权限

设置文件权限的系统调用:

#include <sys/types.h>  
#include <sys/stat.h>  

int chmod(const char *path, mode_t mode);  
int fchmod(int fd,mode_t mode);  

成功返回0,失败返回-1,并设置errno:

  • EACCESS:没有搜索path的权限
  • EBADF:fd不合法的文件描述符(仅fchmod)
  • EFAULT:path不合法的指针(仅chmod)
  • EIO:文件系统内部I/O错误
  • ELOOP:由于symbolic link导致解析path死循环
  • ENAMETOOLONG:path太长(仅chmod)
  • ENOENT:path不存在
  • ENOME:内存不足
  • ENOTDIR:不是一个目录(仅chmod)
  • EPERM:进程不是文件的owner或者缺少CAP_FOWNER能力。
  • EROFS:文件在只读文件系统中。

文件的所有者

stat结构中st_uid和st_gid提供了文件的所有者和group,以下系统调用可以改变所有者:

#include <sys/types.h>  
#include <unistd.h>  

int chown(const char *path, uid_t owner, gid_t group);  
int lchown(const char *path, uid_t owner, gid_t group);  
int fchown(int fd, uid_t owner, gid_t group);  

chown和lchown区别:chown会follow simbolic link,改变link的目标文件,lchmod改变符号链接本身。
成功过返回0,失败返回-1,并设置errno。

struct group *gr;  
int ret;  

gr = getgrnam("officers");  
if(! gr){  
    perror("getgrnam");  
    return 1;  
}  

ret = chmod("manifest.txt",-1,gr->gr_gid);  
if(ret){  
    perror("chmod");  
    return 1;  
}  

设置为root拥有者:

int make_root_owner(int fd){  
    int ret;  

    ret = fchown(fd,0,0);  
    if(ret)  
        perror("fchown");  
    return ret;  
}  

进程需要有CAP_CHOWN能力,也就是必须是root为所有。

文件系统Navigation

改变当前的工作目录

#include <unistd.h>  

int chdir(const char *path);  
int fchdir(int fd);  

将当前的工作目录指定为path:

char *dir = "/tmp";  
if(chdir(dir) == -1)  
    perror("Failed to change current working directory to /tmp");  

获得当前工作目录

#include <unistd.h>  

char *getcwd(char *buf, size_t size);  

例子: 

#include <limits.h>  
#include <stdio.h>  
#include <unistd.h>  
#ifndef PATH_MAX  
#define PATH_MAX 255  
#endif  

int main(void)
{  
    char mycwd[PATH_MAX];  
    if(getcwd(mycwd,PATH_MAX) == NULL){  
        perror("Failed to get current working directory");  
        return 1;  
    }  
    printf("Current working directory: %s\n",mycwd);  
    return 0;  
}  

path conf

#include <unistd.h>  

long fpathconf(int fd, int name);  
long pathconf(const char *path,int name);  
sysconf(int name);  

创建目录

#include <sys/stat.h>  
#include <sys/types.h>  

int mkdir(const char *path, mode_t mode);  

成功返回0,失败返回-1,并设置好errno。没有可以递归删除与rm -r等价的系统调用。
如果目录非空,失败设置errno为ENOTEMPTY。

int ret;  

ret = rmdir("/home/fuliang/test");  
if(ret)  
    perror("rmdir");  

删除目录

#include <unistd.h>  

int rmdir(const char *path);  

成功返回0,失败返回-1 

文件夹访问

opendir closedir, readdir

#include <dirent.h>  
DIR *opendir(const char *dirname);  
struct dirent *readdir(DIR *dirp);  
int closedir(DIR *dirp);  
void rewinddir(DIR *dirp);  

例子:显示pathname下的文件:

#include <dirent.h>  
#include <errno.h>  
#include <stdio.h>  

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

    if(argc != 2){  
        fprintf(stderr,"Usage %s directory_name\n", argv[0]);  
    }  

    if((dirp = opendir(argv[1])) == NULL){  
        perror("Failed to open directory");  
        return 1;  
    }  

    while((direntp = readdir(dirp)) != NULL)  
        printf("%s\n", direntp->d_name);  
    while((closedir(dirp) == -1) && (errno == EINTR)) ;  
    return 0;   
}  

dirent结构:

struct dirent{  
    ino_t d_ino; /* inode number */  
    off_t d_off; /* offset to the next dirent */  
    unsigned short d_reclen; /* length of this record */  
    unsigned char d_type; /* type of file */  
    char d_name[256]; /*filename*/    
};  

POSIX仅需要d_name字段,其他的都是可选的或者是linux特有的。
可移植性的程序应该只访问d_name字段。

从Dir可以获得文件描述符:

#define _BSD_SOURCE  
#include <sys/types.h>  
#include <dirent.h>  

int dirfd(DIR *dir);  

这个是BSD的一个扩展,并不是POSIX标准。

访问文件的状态信息

lstat、stat

硬链接和软链接

硬链接:两个path指向同一个inode.
软链接:单独的一个文件,里面存储了链接文件的路径。

创建和删除一个硬链接

#include <unistd.h>  

int link(const char *path1, const char *path2);  
int unlink(const char *path);  

为有path1指定的文件创建一个新的目录项。

#include <stdio.h>  
#include <unistd.h>  

if (link("/dirA/name1","dirB/name2") == -1)  
    perror("Failed to make a new link in /dirB");  

创建和删除一个硬链接

#include <unistd.h>  

int symlink(const char *path1, const char *path2);  

拷贝或者移动文件

拷贝和移动文件时两个最基本的文件操作任务,可以使用shell命令cp和mv来完成。

拷贝文件

Unix没有提供此操作的系统调用或者库来完成拷贝文件和目录,但可以使用cp命令来手工执行这个任务。
拷贝一个文件src到目标dst,要执行的操作步骤:

  1. 打开src
  2. 打开dst,创建如果不存在,清空如果存在。
  3. 将src读取到内存
  4. 将从src读取到的内容写入dst
  5. 继续指导所有内容从src写到dst
  6. 关闭dst
  7. 关闭src

如果拷贝一个目录,递归拷贝目录本身及其子目录通过mkdir及其采用上面步骤拷贝文件。

移动

Unix提供了移动文件的系统调用。ANSI C使用这个调用来移动文件,POSIX标准将其用于文件和目录。

#include <stdio.h>  

int rename(const char *oldpath, const char *newpath);  

将路径名称从oldpath改成newpath,内容和inode没有变化。newpath需要在同一文件系统中。
成功返回0,失败返回-1,并设置errno。

创建临时文件

ISO C标准定义了两个标准I/O函数来创建临时文件。

#include <stdio.h>  

char *tmpnam(char *ptr); /*返回执行路径的指针*/  
FILE *tmpfile(void); /*返回文件指针,错误则为NULL*/  

tmpnam每次产生一个不同的合法的路径名字。如果ptr是NULL,生成一个存储在静态区域的名字,
并返回指向它的指针,所以如果连续调用两次会覆盖前一次的tmpname。
如果指定了ptr,则它被假设指向一个数组,并且长度不小于L_tmpnam(<stdio.h>中定义)。
例子:

#include <stdio.h>  

int main(void){  
    char name[L_tmpnam],line[MAXLINE];  
    FILE *fp;  

    printf("%s\n",tmpnam(NULL));  
    tmpname(name);  
    printf("%s\n",name);  

    if((fp = tmpfile()) == NULL){  
        perror("tmpfile");  
        return 1;  
    }  

    fputs("one line of output\n",fp);  
    rewind(fp);  

    if(fgets(line,sizeof(line),fp) == NULL){  
        perror("fgets");  
        return 1;  
    }  
    fputs(line,stdout);  

    return 0;  
}  

XSI扩展定义了两个另外的方法来操作临时文件:

#include <stdio.h>  

char *tempnam(const char *directory, const char *prefix);  
int mkstemp(char *template);  

tempnam和tmpnam一样,但是可以指定目录和前缀来生成临时文件名。

  1. 如果环境变量定义了TMPDIR,则使用它作为目录。
  2. 如果directory不为NULL,则使用它。
  3. 在<stdio.h>中的P_tmpdir作为目录。
  4. 本地的目录,通常/tmp被作为临时目录。

前缀prefix如果不为NULL,至少5个字节。
生成的名字是通过malloc动态申请的内存,所以要使用free释放。

#include <stdio.h>  

int main(int argc, char *argv[]){  
    if(argc != 3){  
        pintf("usage: %s <directory> <prefix>",argv[0]);  
        return 1;  
    }  

    printf("%s\n",tempnam(argv[1][0] != ' ' ? argv[1] : NULL, argv[2][0] != ' ' ? argv[2] : NULL));  

    return 0;  
}  

mkstemp和tmpfile类似,只是返回了文件描述符,并且mkstemp生成的临时文件不会自动清除。
path的后六个字符要设置为XXXXXX,通过替换它来生成不同的名字。


专注于用户空间的系统级编程–即内核之上的所有内容。

线程是指在单个进程内,多路并行执行的创建和管理单元。由于线程引入了数据竞争和死锁,相关的编程错误不计其数。

进程和线程

现代操作系统包括了两种对用户空间的基础的虚拟抽象:虚拟内存和虚拟处理器
它们使进程认为自己独占机器资源。

虚拟内存为每个进程提供独立的内存地址空间,该内存地址连续映射到物理内存上。
虚拟处理器使得进程“感觉”只有自己正在运行,操作系统对其“隐藏”了事实:多个进程在可能多个处理器上以多任务方式同时运行。

虚拟内存是和进程相关的,与线程无关。
因此,每个进程有独立的内存空间,而进程中的所有线程共享这份空间。

虚拟处理器是和线程相关的,与进程无关。每个线程都是可独立调度的实体,支持单个进程每次“处理”多个操作。

和进程一样,线程也“感觉”自己独占一个处理器。
但是,和进程不一样的是,线程没有“感觉”自己独占内存–进程中的所有线程共享内存空间。

多线程

进程是正在运行的程序的抽象。但是,为什么要分离执行单元,引入线程?多线程机制有以下优点:

  • 编程抽象:将工作分为多个模块,每个模块分配一个执行单元(线程)。
    利用这种方法的设计模式包括“每个连接一个线程”和线程池模式
  • 并发性:对多处理器提供了实现“真正并发”的高效方式。
    每个线程有自己的虚拟处理器,作为独立的调度实体,可以在多处理器上运行多个线程,从而提高系统吞吐量
  • 提高响应能力:单线程进程中,一个长时间运行的任务会影响应用对用户输入的响应,多线程中可以有一条线程响应用户输入
  • I/O阻塞:单线程中,I/O阻塞会影响整个进程
  • 上下文切换:线程间切换的代价显著低于进程间的上下文切换
  • 内存保存:线程提供了一种可以共享内存,并同时利用多个执行单元的高效方式

上下文切换:线程的一大性能优势是同一进程内的线程之间切换代价很低。
在任何系统上,进程内切换的代价低于进程间切换,前者通常是后者的一部分。
在非Linux系统上,这种差别非常明显,进程间通信(IPC)代价非常高。
因此,很多系统上,称线程为“轻量级进程”。在Linux中,进程间切换代价并不高,而进程内切换的成本接近于0:
接近进入和退出内核的代价。进程的代价不高,但是线程的代价更低。

线程模型

在系统上实现线程模型的方式有好几种,因内核和用户空间提供的支持而有一定程度的级别差异。

最简单的是内核为线程提供了本地支持的情况,每个内核线程直接转换成用户空间的线程。
这种模型称为“1:1线程模型”,也称为“内核级线程模型”。
在Linux中只是简单的将线程实现成能够共享资源的进程。
线程库通过系统调用clone()创建一个新的线程,返回的“进程”直接作为用户空间的线程。

“用户级线程模型”是“N:1线程模型”,一个N个线程的进程只会映射到一个内核进程。

“混合式线程模型”是“N:M线程模型”,上述两中的综合,但是模型过于复杂。

Linux上普遍的线程模型是第一种。

线程模式

创建多线程应用最重要的一步是确定线程模式,线程模式也是应用程序的逻辑处理和I/O模式,
可能存在很多抽象和实现细节,但两个核心的编程模式是:

  • 每个连接对应一个线程(thread-per-connection)
  • 事件驱动(event-driven)

并发性、并行性和竞争

并发性与并行性??

竞争条件是指由两个或多个线程对共享资源的非同步访问而导致错误的程序行为。
共享资源可以是系统硬件、内核资源或内存中的数据。后者最常见,称为数据竞争(date race)。
竞争发生的窗口–需要同步的代码–成为“临界区”。

同步,互斥和死锁

最常用来完成同步,实现临界区原子操作的技术是锁(lock),在Pthreads中称之为“互斥(mutexes)”。

锁住数据,而不是代码,当锁和代码关联起来时,锁的语义就很难理解。
随着时间变化,锁和数据之间的关系就不会很清晰,程序员会为数据引入新的使用方式,而不会包含相应的锁。
把锁和数据关联,这个映射就会很清晰。

互斥的引入会带来编程BUG:死锁。

避免死锁需要从一开始的设计中为多线程设计好锁的机制。
互斥体应该和数据关联,而不是和代码关联,从而有清晰的数据层。
针对死锁(ABBA)问题,需要明确互斥获取的顺序。

Pthreads

Linux内核只为线程的支持提供了底层原语,比如clone()系统调用。
POSIX对线程库进行了标准化,称之为POSIX线程,简称为Pthreads。
是UNIX系统上C/C++语言的主要线程解决方案。

Pthread API

Pthreads是由glibc提供,存在于独立库libpthread中,使用时需要显示链接:

gcc -Wall -Werror -pthread b.c -o b

Pthread API在文件<pthread.h>中定义,每个函数前缀都是pthread_,可以分为两大类:
线程管理(创建、销毁、连接和detach线程等)同步(互斥、条件变量等)

#include <pthread.h>

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);
int pthread_join (pthread_t tid, void ** status);
pthread_t pthread_self (void);
int pthread_equal(pthread_t t1, pthread_t t2);
int pthread_detach (pthread_t tid);
void pthread_exit (void *status);
int pthread_cancel(pthread_t thread);

int pthread_mutex_lock(pthread_mutex_t * mptr); 
int pthread_mutex_unlock(pthread_mutex_t * mptr); 

int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr); 
int pthread_cond_signal(pthread_cond_t *cptr); 
int pthread_cond_broadcast (pthread_cond_t * cptr);
int pthread_cond_timedwait (pthread_cond_t * cptr, pthread_mutex_t *mptr, const struct timespec *abstime);

pthread_create用于创建一个线程,成功返回0,否则返回错误码(不使用errno)。

  • pthread_t tid:线程id的类型为pthread_t,通常为无符号整型,当调用pthread_create成功时,通过tid指针返回。
  • const pthread_attr_t *attr:指定创建线程的属性,如线程优先级、初始栈大小、是否为守护进程等。
    可以使用NULL来使用默认值,通常情况下我们都是使用默认值。
  • void (func) (void *):函数指针func,指定当新的线程创建之后,将执行的函数。
  • void *arg:线程将执行的函数的参数。如果想传递多个参数,请将它们封装在一个结构体中。

pthread_join用于等待某个线程退出,成功返回0,否则返回错误码(不使用errno)。
成功调用时,调用线程会被阻塞,直到由tid指定的线程终止,用于线程同步。

  • pthread_t tid:指定要等待的线程ID
  • void ** status:如果不为NULL,那么线程的返回值存储在status指向的空间中(这就是为什么status是二级指针的原因!这种才参数也称为“值-结果”参数)。

默认情况下,线程是创建成可join的。
pthread_detach用于是指定线程变为分离状态(不可join),就像进程脱离终端而变为后台进程类似。
成功返回0,否则返回错误码(不使用errno)。变为分离状态的线程,如果线程退出,它的所有资源将全部释放。
而如果不是分离状态,线程必须保留它的线程ID,退出状态直到其它线程对它调用了pthread_join。

pthread_join()或pthread_detach()都应该在进程中每个线程上调用,这样当线程终止时,也会释放资源。

pthread_self用于返回当前线程的ID

pthread_equal用于比较两个线程ID是否一样,如果不同返回0

pthread_exit用于终止线程,可以指定返回值,以便其他线程通过pthread_join函数获取该线程的返回值。

pthread_cancel用于通过其他线程调用来结束线程。

在对临界资源进行操作之前需要pthread_mutex_lock先加锁,操作完之后pthread_mutex_unlock再解锁。
而且在这之前需要声明一个pthread_mutex_t类型的变量,用作前面两个函数的参数。

int withdraw(struct account *account, int amount)
{
    pthread_mutex_lock(&account->mutex);
    const int balance = account->balance;
    if(balance < amount)
    {
        pthread_mutex_unlock(&account->mutex);
        return -1;
    }

    account->balance = balance - amount;
    pthread_mutex_unlock(&account->mutex);

    disburse_money(amount);

    return 0;
}

在这个例子中,不是使用全局锁,而是在结构体中定义一把锁,这样每个account实例都有自己的锁,实现对数据的加锁。

条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。
条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
条件变量要和互斥量相联结,以避免出现条件竞争–一个线程预备等待一个条件变量,
当它在真正进入等待之前,另一个线程恰好触发了该条件。

References


专注于用户空间的系统级编程–即内核之上的所有内容。

进程调度

进程调度器是内核子系统,用于将有限的处理器资源分配给系统中的各个进程。

多任务操作系统分为协同式(cooperative)抢占式(preemptive)

协同式多任务系统中,进程会一直运行到自己结束。这种自发结束的行为称为“让出(yielding)”。
操作系统不会强制要求让出。

抢占式多任务系统中,调度器决定某个进程何时停止运行,而由另一个进程运行,这种行为成为“抢占”。
进程在被抢占前所能够运行的时间成为该进程的“时间片(timeslice)”。

现代操作系统基本上都使用抢占式多任务机制。

完全公平调度器

当前Linux进程调度器是在Linux内核版本2.6.23发布的,称为“完全公平调度器(Completely Fair Scheduler,CFS)”。
采用“公平入队(fair queuing)”调度算法,对竞争进程采取公平访问资源的策略。

和传统的UNIX进程调度器有很大区别,消除了时间片作为处理器访问分配单元,而是给每个进程分配了处理器的时间比例。

让出处理器

#include <sched.h>
int sched_yield(void);

系统调用,支持进程主动让出处理器,并通知调度器选择新的进程来运行。
sched_yield()这个函数可以使用另一个级别等于或高于当前线程的线程先运行。
如果没有符合条件的线程,那么这个函数将会立刻返回然后继续执行当前线程的程序。

使用sched_yield()的两种情况:

  • 用户空间线程的锁定。如果一个线程试图取得另一个线程所持有的锁,
    则新的线程应该让出处理器知道该锁变为可用。
    用户空间锁没有内核的支持,这是一个最间单、最有效率的做法。
    但是现在Linux线程实现引入一个使用futexes的优化解决方案。
  • 处理器密集型程序可用周期性调用sched_yield,
    试图将该进程对系统的冲击减到最小。但是存在两个缺点:系统调度应该由调度器来承担,而不是用户进程。
    而且减轻处理器密集应用带来的负担,是用户的责任,而不是某个应用。

进程优先级

CFS中进程的nice value会决定进程会运行多长时间,或者说是占用的百分比。
可以通过系统调用nice来设置、获取进程的nice value。
该值的范围是(-20,19],越低的值越高的优先级,实时进程应该是负数。

#include <unistd.h>
int nice(int inc);

nice()调用成功会在现有nice value上增加inc,并返回更新之后的值。
只有拥有CAP_SYS_NICE权限的进程(实际上即进程所有者为root)才可以使用负值inc,减少nice value。
从而提升该进程的优先级。因此,非root用户的进程只能降低优先级(增加nice value)。

因为nice()返回可以是-1,那么在判断是否系统调用失败的时候就要综合ret和errno。
调用前将errno值置为0,然后再检查errno:

int ret;

errno = 0;
ret = nice(10); /*increase our nice by 10*/
if(ret == -1 && errno != 0)
    perror("nice");
else
    printf("nice value is now %d\n", ret);

还有两个系统调用可以更灵活地设置,
getpriority可以获得进程组、用户的任何进程中优先级最高的。
setpriority将所指定的所有进程优先级设置为prio:

#include <sys/time.h>
#include <sys/resource.h>

int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
  • which取值为PRIO_PROCESS、PRIO_PGRP或PRIO_USER
  • who指定了进程ID、进程组ID或用户ID,当值为0时,调用分别在当前进程、当前进程组或当前用户上运行

处理器亲和力(Affinity)

Linux支持具有多个处理器的单一系统。在SMP上,系统要决定每个处理器上要运行那些程序,这里有两项挑战:

  • 调度程序必须想办法充分利用所有的处理器
  • 切换程序运行的处理器是需要代价的

进程会继承父进程的处理器亲和性,Linux提供两个系统调用用于获取和设定“硬亲和性”:

int ret;
cpu_set_t set;

CPU_ZERO(&set);
ret = sched_getaffinity(0, sizeof(cpu_set_t), &set);
if(ret == -1)
    printf("调用失败!\n");
for(i = 0; i < 10; i++){
    int cpu = CPU_ISSET(i, &set);
    printf("cpu=%i is %s\n", i, cpu?"set":"unset");
}

CPU_ZERO(&set);
CPU_SET(0, &set);   /*allow CPU #0*/
CPU_CLR(1, &set);   /*disallow CPU #1*/
ret = sched_setaffinity(0, sizeof(cpu_set_t), &set);
if(ret == -1)
    printf("调用失败!\n");
for(i = 0; i < 10; i++){
    int cpu = CPU_ISSET(i, &set);
    printf("cpu=%i is %s\n", i, cpu?"set":"unset");
}

实时系统

Linux除了正常的默认调度策略,还提供两种实时调度策略,头文件<sched.h>中宏定义:

  • SCHED_FIFO,“先进先出”策略
  • SCHED_RR,“轮询”策略(round-robin)
  • SCHED_OTHER,标准调度策略,是非实时进程的默认调度策略,上述两种调度策略进程可以抢占它

获得和设置调度策略的系统调用:

#include <sched.h>

struct sched_param
{
    /* ... */
    int sched_priority;
    /* ... */
};

int sched_getscheduler(pid_t pid);
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *sp);

sched_getscheduler()用法:

int policy;

/* Get our scheduling policy */
policy = sched_getscheduler(0);
switch(policy){
    case SCHED_OTHER:
        printf("Policy is normal.\n");
        break;
    case SCHED_RR:
        printf("Policy is round-robin.\n");
        break;
    case SCHED_FIFO:
        printf("Policy is first-in, first-out.\n");
        break;
    case -1:
        printf("sched_getscheduler failed.\n");
        break;
    default:
        printf("Unknow policy\n");

sched_setscheduler()用法:

int ret;
struct sched_param sp;

sp.sched_priority = 1;
ret = sched_setscheduler(0, SCHED_RR, &sp);
if(ret == -1)
    printf("sched_setscheduler failed.\n");
if(errno == EPERM)
    printf("Process don't the ability.\n");

sched_getparam和sched_setparam接口可用于取得、设定一个已经设定好的调度策略参数:

int ret, i;
struct sched_param sp;

sp.sched_priority = 1;
ret = sched_setparam(0, &sp);    
if(ret == -1)
    printf("sched_setparam error.\n");

ret = sched_getparam(0, &sp);
if(ret == -1)
    printf("sched_getparam error.\n");

Linux提供两个用于取得有效优先值的范围的系统调用:

int min, max;
struct sched_param sp;

min = sched_get_priority_min(SCHED_RR);
if(min == -1)
    printf("sched_get_priority_min error.\n");

max = sched_get_priority_max(SCHED_RR);
if(max == -1)
    printf("sched_get_priority_max error.\n");

printf("SCHED_RR priority range is %d-%d\n", min, max);

资源限制

Linux对进程加上了若干资源限制,这些限制是一个进程所能耗用的内核资源的上限。限制的类型如下:

  • RLIMIT_AS:地址空间上限
  • RLIMIT_CORE:core文件大小上限
  • RLIMIT_CPU:可耗用CPU时间上限
  • RLIMIT_DATA:数据段与堆的上限
  • RLIMIT_FSIZE:所能创建文件的大小上限
  • RLIMIT_LOCKS:文件锁数目上限
  • RLIMIT_MEMLOCK:不具备CAP_SYS_IPC能力的进程最多将多少个字节锁进内存
  • RLIMIT_MSGQUEUE:可以在消息队列中分配多少字节
  • RLIMIT_NICE:最多可以将自己的友善值调多低
  • RLIMIT_NOFILE:文件描述符数目的上限
  • RLIMIT_NPROC:用户在系统上能运行进程数目上限
  • RLIMIT_RSS:内存中页面的数目的上限
  • RLIMIT_RTTIME:最多可以消耗CPU时间上限
  • RLIMIT_RTPRIO:不具备CAP_SYS_NICE能力进程所能请求的实时优先级的上限
  • RLIMIT_SIGPENDING:在队列中信号量的上限,Linux特有的限制
  • RLIMIT_STACK:堆栈大小的上限

获取或设置这些限制的方法:

int ret;
struct rlimit rlim;

rlim.rlim_cur = 32*1024*1024;
rlim.rlim_max = RLIM_INFINITY;
ret = setrlimit(RLIMIT_CORE, &rlim);

ret = getrlimit(RLIMIT_CORE, &rlim);
if(ret == -1)
    printf("getrlimit error.\n");
printf("RLIMIT_CORE limits: soft=%ld hard=%ld\n", rlim.rlim_cur, rlim.rlim_max);


专注于用户空间的系统级编程–即内核之上的所有内容。

程序、进程和线程

程序(program)是指编译过的、可执行的二进制代码,保存在介质上,不运行。

进程(process)是指正在运行的程序。进程包括二进制镜像,加载到内存中,
还涉及到虚拟内存实例、内核资源、安全上下文,以及一个或多个线程。

线程(thread)是进程内的活动单元。每个线程包含自己的虚拟存储器,包括栈、进程状态如寄存器,以及指令指针。

在单线程的进程中,进程即线程。一个进程只有一个虚拟内存实例,一个虚拟处理器。
在多线程的进程中,由于虚拟内存是和进程关联的,所有线程共享相同的内存地址空间。

进程ID

创建新进程的那个进程称为父进程,而新进程称为子进程。

获取进程ID和父进程ID:

#include <sys/types.h>
#inlcude <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

运行新进程

在UNIX中,将程序载入内存并执行程序映像的操作与创建新进程的操作是分离的。
一次系统调用会将二进制程序加载到内存中,替换地址空间原来的内容,
并开始执行。这个过程成为“执行(executing)”一个新的程序,是通过exec系列函数完成。

另一个不同的系统调用用于创建新的进程,相当于复制其父进程。
通常情况下,新的进程会立即执行新的程序
创建新进程的操作称为派生(fork),是系统调用fork()来完成的。

在新进程中执行一个新的程序需要执行两个步骤:fork()和exec系列函数。

exec系统调用

函数原型如下:

#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);

exec系列函数只有execve()是真正的系统调用,其他都是基于该系统调用在C库中的封装函数。

这些函数如果调用成功则加载新的程序并跳转到新的程序入口点开始执行,不再返回,如果调用出错则返回-1。
所以,exec函数只有出错的返回值而没有成功的返回值

函数名和参数解析:

  • l和v分别表示参数以列表或数组(向量)方式提供,两种形式的最后一个参数必须为NULL。
    由于execl、execlp、execle这三个函数都是可变参数函数,所以NULL应该转换为(char )NULL或(char)0
  • p表示会在用户的绝对路径path下查找可执行文件,不需要输入完整路径
  • …表示可变长度的参数列表
  • e表示会为新进程提供新的环境变量

exec示例代码:

#include <unistd.h>   

char *const ps_argv[] = {"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};   
char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};  

execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);   
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", (char*)0); 
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);   

execv("/bin/ps", ps_argv);    
execvp("ps", ps_argv); 
execve("/bin/ps", ps_argv, ps_envp); 

函数关系图如下:

fork()系统调用

通过fork()系统调用可以创建一个和当前进程一样的新进程:

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

当fork()调用成功时,会创建一个新的进程,几乎与调用fork()的进程完全相同。
这两个进程都会继续运行,调用者进程从fork()返回后,还是照常运行。此处产生分叉点!

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

  • 在父进程中,fork返回新创建子进程的进程ID;
  • 在子进程中,fork返回0;
  • 如果出现错误,fork返回一个负值-1;

可以通过fork返回的值来判断当前进程是子进程还是父进程,fork()系统调用用法:

pid_t pid;

pid = fork();
if(pid > 0)
    printf("I am the parent of pid=%d!\n", pid);
else if(0 == pid)
    printf("I am the child!\n");
else if(-1 == pid)
    printf("fork");

一个进程调用fork()函数后,系统先给新的进程分配资源,
例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,
只有少数值与原来的进程的值不同。相当于克隆了一个自己。
在Linux中,采用写时复制(copy-on-write,COW)的方式,而不是对父进程空间的整体复制。

终止进程

标准函数,用于终止当前进程:

#include <stdlib.h>

void exit(int status);

status值为0表示终止程序成功,用非0值表示异常终止。
标准C语言中数值0和宏EXIT_SCCESS的值表示终止成功,
宏EXIT_FAILURE的值表示终止不成功,其他值的含义由实现定义。
从函数main返回一个整数值相当于用这个值调用exit函数。

在终止进程之前,C库会安顺序执行一下操作步骤:

  • 所有通过atexit()或on_exit()函数(废止?)注册的函数按与注册时相反的顺序调用,注册几次就调用几次。
  • 刷新打开的是输出流,关闭所有打开的数据流。
  • 删除tmpfile()函数生成的临时文件。
  • 完成用户空间需要做的所有工作,调用系统调用_exit(),内核可以处理终止进程的剩余工作。

很多时候我们需要在程序退出的时候做一些诸如释放资源的操作,但程序退出的方式有很多种,
比如main()函数运行结束、在程序的某个地方用exit()结束程序、用户通过Ctrl+C或Ctrl+break操作来终止程序等等,
因此需要有一种与程序退出方式无关的方法来进行程序退出时的必要处理。
方法就是用atexit()函数来注册程序正常终止时要被调用的函数。

atexit()函数的参数是一个函数指针,
函数指针指向一个没有参数也没有返回值的函数。atexit()的函数原型是:

int atexit (void (*function)(void));

在一个程序中最多可以用atexit()注册32个处理函数,
这些处理函数的调用顺序与其注册的顺序相反,也即最先注册的最后调用,最后注册的最先调用。
如果进程调用了exec函数,会清空所注册的函数列表。
如果进程是通过信号结束(SIGTERM和SIGKILL),就不会调用这些注册的函数。

等待子进程结束

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

僵尸进程:已经终止,但是它的父进程还没有获取到其状态的进程。
僵尸进程会消耗一些系统资源–仅仅是描述进程之前状态的一些概要信息。
主要是为父进程查询子进程状态时提供相应信息。
一旦父进程得到了想要的信息,内核就清除这些信息,僵尸进程终止。

创建并等待新进程

#include <stdlib.h>

int system(const char *command);

创建新进程并等待它结束,可以看作是同步创建进程。

调用system()会执行参数command所提供的命令,而且还可以为该命令指定参数。
“/bin/sh -c”会作为前缀添加到command参数前面。
通过这种方式,再把整个命令传递给shell。

实际上system()函数执行了三步操作:

  • fork一个子进程
  • 在子进程中调用exec函数去执行command
  • 在父进程中调用wait去等待子进程结束

会话(Session)和进程组

进程组:

  • 每个进程都属于某一个进程组
  • 每个进程组都有一个进程组号,该号等于该进程组组长的PID号
  • 一个进程只能为它自己或子进程设置进程组ID号

会话(session)是一个或多个进程组的集合。
setsid()函数可以建立一个会话:

如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话。

  • 此进程变成该对话期的首进程
  • 此进程变成一个新进程组的组长进程。
  • 此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。
    如果该进程是一个进程组的组长,此函数返回错误。
  • 使任何进程不成为组长的方式:先调用fork()然后exit()父进程,此时只有子进程在运行

守护进程

Linux Daemon(守护进程)是运行在后台的一种特殊进程。
它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。
Linux系统的大多数服务器就是通过守护进程实现的。
常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。
守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,
所以它是一个由init继承的孤儿进程。
守护进程是非交互式程序,没有控制终端,所以任何输出,
无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

守护进程不想是任何已存在会话的成员,也不想拥有控制终端,创建守护进程所需步骤:

  • 调用fork,创建新进程,该进程将会成为守护进程
  • 在守护进程的父进程中调用exit。确保父进程的父进程在其子进程结束时会退出,
    保证了守护进程的父进程不再继续运行,而且守护进程不是首进程
  • 调用setsid函数,使守护进程有一个新的进程组和会话,并作为两者的首进程。
    保证了不存在和守护进程相关连的控制终端
  • 调用chdir函数,让根目录成为守护进程的工作目录
  • 关闭所有文件描述符
  • 打开文件描述符0、1和2,并重定向到/dev/null

代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/fs.h>

int main(void)
{
    pid_t pid;
    int i;

    //Create New Process
    pid = fork();
    if(-1 == pid)
        return -1;
    else if(0 == pid)
        exit(0);

    //Create new session and process group
    if(setsid() == -1)
        return -1;

    //Set the working dir to "/"
    if(chdir("/") == -1)
        return -1;

    //Close all open files
    for(i = 0; i < NR_OPEN; i++)
        close(i);

    //Redirect fd's 0,1,2 to /dev/null
    open("/dev/null", O_RDWR);  //stdin
    dup(0); //stdout
    dup(0); //stderr

    //do its daemon thing ...
}

其实我们完全可以利用daemon()函数创建守护进程,其函数原型:

#include <unistd.h>

int daemon(int nochdir, int noclose);

参数:

  • nochdir:=0将当前目录更改至“/”
  • noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”