T9的空间

You will never walk alone!

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  69 随笔 :: 0 文章 :: 28 评论 :: 0 Trackbacks

#

线程控制

APUE讲的destroy会free空间,这件事情看起来不太对,也许是Base on实现
起码我看到的实现是没有做这件事情的。

 

int pthread_attr_init(pthread_attr_t * attr)
{
    
*attr = gDefaultPthreadAttr;
    
return 0;
}


int pthread_attr_destroy(pthread_attr_t * attr)
{
    memset(attr, 
0x42sizeof(pthread_attr_t));
    
return 0;
}


Attribution有下面这些。

 

typedef struct
{
    uint32_t flags;
    
void * stack_base;
    size_t stack_size;
    size_t guard_size;
    int32_t sched_policy;
    int32_t sched_priority;
}
 pthread_attr_t;

我们可以用malloc和mmap给Thread分配stack,指定stack大小和位置(低地址)

mutex属性
两个东西
1.进程共享属性,PTHREAD_PROCESS_PRIVATE or SHARED
意思是说如果这个Mutex是分配在两个进程共享的memory,然后设置为shared mutex就可以用于进程同步
2.类型属性,normal;errorcheck;recurive
意思是定义Mutex本身的特性,这里更正下前面那章提到的错误
只有normal的mutex才会在再次加锁时发生deadlock
errorcheck的会直接返回错误
recurive本身就允许,会有计数

rw lock和condition属性
只支持第一个

TLS变量
先看下TLS的位置

 * +---------------------------+
 
* |     pthread_internal_t    |
 
* +---------------------------+
 
* |                           |
 
* |          TLS area         |
 
* |                           |
 
* +---------------------------+
 
* |                           |
 
* .                           .
 
* .         stack area        .
 
* .                           .
 
* |                           |
 
* +---------------------------+
 
* |         guard page        |
 
* +---------------------------+

pthread_internal_t是记录线程自己本身的一些属性的变量,guard page就是线程attr上设置的防止栈溢出的扩展内存

创建TLS变量的方法
1.创建Key,这个Key是这个Process中所有Thread可以共享访问的,然后每个Thread自己去关联自己的Thread Local变量
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void*))
创建key的时候需要保证唯一调用,也就是不会有两个Thread同时调用,然后发生一些base on实现的不确定的结果,可以使用pthread_once

pthread_once的实现很简单,用mutex做互斥量保证pthread_once_t的访问,然后利用pthread_once_t做flag来调用init_routine

int  pthread_once( pthread_once_t*  once_control,  void (*init_routine)(void) )
{
    
static pthread_mutex_t   once_lock = PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
    
volatile pthread_once_t* ocptr = once_control;

    pthread_once_t tmp 
= *ocptr;
    ANDROID_MEMBAR_FULL();
    
if (tmp == PTHREAD_ONCE_INIT) {
        pthread_mutex_lock( 
&once_lock );
        
if (*ocptr == PTHREAD_ONCE_INIT) {
            (
*init_routine)();
            ANDROID_MEMBAR_FULL();
            
*ocptr = ~PTHREAD_ONCE_INIT;
        }

        pthread_mutex_unlock( 
&once_lock );
    }

    
return 0;
}

所以这个pthread_once_t必须是全局or静态变量
pthread_once最常用的case除了拿到TLS key外,init函数经常会使用这个,避免多线程同时调用。

然后就是thread cancel
Android Bionic库没有实现 pthread_setcancelstate(int state, int* oldstate)以及pthread_cancel()
这里thread的取消,有些是取消不掉的,例如一些blocking call,所有的取消均是需要设定一些取消点的,

有人有专门针对Android没有Thread Cancel来想办法
基本就是thread signal,在signal handler中去call pthread_exit();
or用PIPE在多路复用中break出来
http://blog.csdn.net/langresser/article/details/8531112

如果信号与硬件故障or计时器超时相关,那么信号会发送到引起事件的该线程中去,其他信号会发送到任意一个线程。
这里是说的POSIX线程模型,一般的信号都会发送给进程中没有阻塞该信号的某个线程。APUE讲的某个没有阻塞该信号的线程,其实有点模糊。

但是Linux的实现不一样,linux的thread是clone出来的,就是共享资源的独立进程,这样子其实蛮自然的,也不容易复杂。
linux中,有可能线程就不会注意到该信号,这样讲也有点模糊,总之终端驱动程序的信号会将信号通知到进程组,这样子所有的thread就会都收到信号
如果你不想所有的线程都去关心信号,那么可以使用一个专门处理信号的线程
先将其他线程的signal都pthread_sigmask SIG_BLOCK掉,然后使用sigwait在特定的线程来等
pthread_sigmask和sigprocmask很像

thread与fork
fork后的子进程会继承mutex, rw lock, condition的状态
但是子进程中只会存在一个线程,也就是那个调用fork的线程的副本,有个复杂的事情是关于上面继承下来的那些东西,子进程由于只有一个线程,没办法知道
上面那三个东西的具体状态,需要有个清理锁的动作 -->pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void))
我目前不知道这个东西的使用场景,而且不容易控制。
因为一般如果我们fork后都会接execu,这样地址空间就会改变,那么这些继承的东西就没用了。

然后有谈一点关于线程IO的东西
pwrite/pread
也就是原子的进行lseek和r/w,但是这个并不能保证大家常见的冲突问题,也就是一个thread在写另外一个要读,所以这两个东西并不能解决同步的问题。

作业:
12.3 理论上函数开始时屏蔽所有信号,函数结束时恢复,是可以做到异步信号安全的。如果你只是屏蔽一些信号,那么没办法做到,因为如果有其他信号进来,你call的函数里面有一些不可重入的函数,同样不能保证是异步安全的。

12.5 fork可以在拿来执行可执行程序,现在应该就这一个Case。

posted @ 2013-06-04 15:41 Torres 阅读(247) | 评论 (0)编辑 收藏

线程函数
int pthread_equal(pthread_t tid1, pthread_t tid2)
pthread_t pthread_self(void)

int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr
     void* (*start_rtn)(void), void* restrict arg)

thread被创建的时候会继承调用线程的浮点环境和信号屏蔽字,但是该线程的未决信号集将会被清楚,
清除未决信号集这件事情应该是没有疑问的,在thread创建之前pending住的信号,不应该deliver给下一个
Ps: 线程的浮点环境指的是啥? 看来以后我应该去注意下浮点数的运算原理。

pthread相关的函数会直接返回错误码,而不会和一些system call一样,置全局errno,两种方式都有好处,一个可以讲返回值
带回的错误代码集中起来,范围缩小;另外一个非常方便,关键点在于这一类共用errno的是否真的异常是可以共用的。

pthread_create返回之前有可能新的线程就已经开始run了

启动函数 void* (*start_rtn)(void)

可以通过return给回来,也可以通过pthread_exit给
这个会存在一个地方
通过pthread_join(tid, void**)取回来

这里join的和java join是一样的功能

如果这个东西是一个很大的东西:),那么放到heap是最好的选择,不要放到stack上了,不然线程返回这东西就没了,join取到的内存地址就变成一个无效的了,SIGSEGV就会被发出来

pthread_cancel,同一个进程可以call,提出请求终止线程

pthread_cleanup_push
pthread_cleanup_pop

线程分离,这样子线程终止后可以释放一些资源,而不用一定要其他人来join
方法有两种,新建的时候加上分离属性
    pthread_attr_init (&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    ret = pthread_create(&s_tid_dispatch, &attr, eventLoop, NULL);

或者call pthread_detach(pthread_t tid)

线程互斥与同步

typedef struct
{
    
int volatile value;
}
 pthread_mutex_t;

多注意volatile变量,这个东西理论上就是让编译器不要做优化,不要cache volatile类型的变量,
每次都去内存地址中拿,而不是寄存器/高速缓存副本,这种变量极容易被编译器不知道的人改变,例如其他线程。

静态初始化:
#define  PTHREAD_MUTEX_INITIALIZER             {0}
#define  PTHREAD_RECURSIVE_MUTEX_INITIALIZER   {0x4000}
#define  PTHREAD_ERRORCHECK_MUTEX_INITIALIZER  {0x8000}
所谓的动态初始化
pthread_mutex_init; pthread_mutex_destroy

然后就是一些pthread mutex的基本处理函数了
lock,unlock
trylock;

这个trylock需要好好理解下,尝试获取lock,如果OK,那么lock他然后 return 0; 否则也不会suspend住,而是直接返回EBUSY

pthread_mutex_destroy, 会先去try lock,然后处理掉这个mutex的值。

这里稍微提一下

int pthread_mutex_trylock(pthread_mutex_t *mutex)
{
    
int mtype, tid, oldv, shared;

    
if (__unlikely(mutex == NULL))
        
return EINVAL;

    mtype  
= (mutex->value & MUTEX_TYPE_MASK);
    shared 
= (mutex->value & MUTEX_SHARED_MASK);

    
/* Handle common case first */
    
if ( __likely(mtype == MUTEX_TYPE_NORMAL) )
    
{
        
if (__atomic_cmpxchg(shared|0, shared|1&mutex->value) == 0{
            ANDROID_MEMBAR_FULL();
            
return 0;
        }


        
return EBUSY;
    }




__likely/__unlikely函数用来告诉编译器优化代码,类似if else中最有可能or最没有可能发生的Case

mutex就有deadlock的问题,单线程,如果有代码重入重复获取锁就会deadlock,因为你走不到你unlock的地方去;另外
常见的deadlock就是lock比较多,又没有设计好顺序,这个应该从业务逻辑上就应该定义好,当然有时候有的人用的时候or改代码的时候
并没有理清这些lock的关系,是否有dependency,比较难通过借用一些编译系统来Cover住,然后改完就有bug。

然后还有一种需要设计好的是锁的粒度
太粗太细都不好
粒度太粗,lock住的东西太多,很多线程都要等lock,最后这个东西会演变成一个串行的东西
粒度太细,lock又变的太多,不停的需要lock/unlock,performance就会变差。
目前看到的Android上的很多lock都太粗。

rw锁 ->读写锁
基本理念就是读不会影响临界区发生变化
所以读模式的rw lock可以多个人占用,写模式的rw lock时能被一个线程lock

只要有写模式lock请求,那么后面的读模式lock请求一般实现是都会被Suspend住,不然因为读模式下,可以重复lock,如果不
suspend,那么写模式的lock请求有可能永远得不到相应。
rw锁一般用在 read比 write行为多的多的场景,允许多线程并发去读,单一线程去写。

然后会想到spinlock,可以去网上search看下基本概念,spinlock一般在SMP架构下会比较有效果。

mutex是一种同步机制or讲这是一种互斥机制 -> Java synchronize
还一种就是条件变量 condition.. -> wait/notify

这里有段话很好
条件变量给多个线程提供了一个回合的场所,条件变量与互斥量一起使用的时候,允许线程以无竞争方式等待特定的条件发生。

作业:
1.线程之间传递数据不要用stack变量,用放到下面这些地方的变量就好,RW/RO/ZI/Heap就没事了
4.
现在一般都是这样

    pthread_mutex_lock(&s_startupMutex);

    s_started = 1;
    pthread_cond_broadcast(&s_startupCond);

    pthread_mutex_unlock(&s_startupMutex);

会在broadcast后才unlock
否则有比较高的概率,unlock后,被其他线程将条件改掉,这个时候broadcast出去就没有意义了。
但是这种也有可能会被另外一个线程,并非wait在那里的线程改变条件值

所以 pthread_cond_wait 返回并不意味着条件一定为真了
最好是always check条件
类似这种
    while (s_started == 0) {
        pthread_cond_wait(&s_startupCond, &s_startupMutex);
    }

posted @ 2013-06-03 17:03 Torres 阅读(171) | 评论 (0)编辑 收藏

信号
提供异步时间处理方式
触发时机:
1.终端命令
2.硬件异常,由kernel抛向对应的Process
3.kill函数/kill命令(超级用户or Process的user相同,这里的user id一般是指实际用户ID or 有效用户ID,如果支持 _POSIX_SAVED_IDS,那么检查saved-user-id)
4.软件触发(满足信号条件)

这里也稍微解释下Kill这个东西,kill函数,kill命令并不是字面上杀掉某些东西,kill只是在特定的时间发送信号,
具体的处理取决于信号本身和信号的处理方式。

信号的处理方式:
这个有点类似Java Exception的处理方式,catch住or往上throw

APUE讲的信号处理方式有三种
1.忽略,SIGKILL/SIGSTOP不能忽略;一般如果Process自己不管的话,应该会走到系统默认的处理流程中,所以不能忽略这件事情是系统自己就会保证的。
2.捕捉信号,例如SIGCHLD,一般parent会call waitpid取子进程终止状态,避免他处理Zombie状态。
3.执行系统默认动作

我感觉1和3可以归为一类吧,只是有些signal没有系统默认动作,然后就跳过去了。一般的系统默认动作也就是终止进程。

SIGKILL/SIGSTOP不能被忽略的原因我觉得讲的不错,提供超级用户终止进程的可靠方法,不然后续有可能进程的行为是未定义的.

signal函数定义:(

void (*signal(int signo, void(*func)(int)))(int);

这个C函数的声明有点难看懂

APUE讲signal的语义与实现有关,所以建议大家都用sigaction

不过我看了下Android Bionic下的实现,分别包含BSD以及SYSV的版本,但也都变成了sigaction,所以一般在C库中就保证了这一点,也就无所谓了。

 

static __sighandler_t
_signal(
int  signum, __sighandler_t  handler, int  flags)
{
    
struct sigaction  sa;
    __sighandler_t    result 
= SIG_ERR;

    sigemptyset( 
&sa.sa_mask );

    sa.sa_handler 
= handler;
    sa.sa_flags   
= flags;

    
if ( !sigaction( signum, &sa, &sa ) )
        result 
= (__sighandler_t) sa.sa_handler;

    
return result;
}



__sighandler_t bsd_signal(
int signum, __sighandler_t handler)
{
  
return _signal(signum, handler, SA_RESTART);
}


__sighandler_t sysv_signal(
int signum, __sighandler_t handler)
{
  
return _signal(signum, handler, SA_RESETHAND);
}



exec函数会讲信号处理方式还原为系统默认,这个应该毫无疑问,exec后地址空间就不一样了,保留之前的捕捉函数也是无意义的;在这之前是有意义的,我的意思是说fork后exec之前。

后面有列一大堆是否可以重入的函数,不太想记,然后讲到了malloc
我想不能重入的函数无外乎两种类型
one: 自己有maintain一些全局or static变量 --> malloc 维护分配内存时static heap linklist
two: 函数参数里面有引用之类的,会影响调用者的情况。

然后我看到这个立马就想到了线程安全,malloc是线程安全的麽?
去看了下Bionic的实现,然后就又看到下面这个名字 Dong Lea,马上就会想到Java Concurrent库,看到这个你直接就会有想法,Bionic里面这个malloc肯定是线程安全的;
有时候被这些东西搞的很累,比较烦他,而且Bionic里面用的malloc的那个实现版本(aka dlmalloc)我又没看懂:(,只好去网上search了一下,有个人做过实验,有些结论还是可以参考的
http://www.360doc.com/content/12/0420/23/168576_205320609.shtml

总的来说,这个东西取决于C库实现,我想Bionic和Glibc都应该一样会支持两种不同的版本,然后编译的时候就可以确定是否有线程相关操作,然后在link的时候link过来不同的版本

线程安全和信号安全是两个概念

如果在线程安全的malloc中,信号处理函数中发生重入,那么应该是会发生dead lock
如果是非线程安全中,那么应该是所谓的 undefined behavior.

前面还有一个概率忘记写
早期的Unix系统,如果系统在执行一个低速系统调用(基本可以总结为blocking IO:包括IPC,File IO,ioctl),那么如果捕捉到信号,那么系统就会中断这个system call -->EINTR, 这在当时是有理由的,而且理由看起来也合理,但是由于user有时候并不知道某些系统调用是否是低速系统调用,BSD引进了自动重启的功能,linux follow这种规则

然后有几个版本的signal函数
signal默认自动重启
_signal(signum, handler, SA_RESTART) 这是由于在sigaction中的flag为SA_RESTART,上面sysV的实现我不想多写了。
sigaction也就是可选的了。

但一般我印象中好多 read/write都是会自己判断返回值以及errno 是否为 EINTR,然后retry,因为上面这种方式依赖系统实现,需要将所有的signal都设定为SA_RESTART,那么如果有某个信号发生的时候,被他INTR的系统调用才会自动重启,不知道默认signal在注册处理行为的时候是不是如此,感觉不太好用。

alarm函数
对于很多像alarm这种函数,在设计时or使用时均应该考虑临界值的问题
alarm的唯一性,是by process的,process单例
所以如果alarm两次,那么第一次会被覆盖,怎么处理第一次未完成的情况->返回值会带回来剩余的时间
怎么取消设定的alarm
传入值为0 means cancel
总之了,这个东西设计的时候还蛮完善的

后面还有一堆有关signal的标准函数,这里也就不一一列举

作业:
1.去掉for(;;), 那捕捉到SIGUSR1就返回了,pause()只要捕捉到信号,等信号处理完时自己就会返回并带回EINTR。不晓得为啥有这种题目...
2.实现sig2str,这有点无聊了.:(不写
3.画runtime stack的样子。
4.IO操作的超时最好不要采用alarm的方式,各种原子问题,select/poll是最好的选择。
后面有好一些是要写代码的...
我略想,然后就不太愿意写了.
PS:有些东西略难...我也是有点想不清楚的样子,打算慢慢搞。

后面决定把标题改一下,顺利看完书吧...Orz.

posted @ 2013-06-02 21:52 Torres 阅读(218) | 评论 (0)编辑 收藏

第九章 进程关系
也是首先记录基本概念

关于linux终端的概念,这个最好仔细想一下,因为之后会有伪终端的内容,如果不想的具体一些,那么会比较难有感觉。下面有个链接,我觉得写的还可以。
http://blog.csdn.net/smstong/article/details/8760331

我写下我的理解,肯定有不太对的地方,但只能到此。

广义的讲: 终端就是用来和User交互的设备,键盘,显示器,打印机...
这里一般讲的是狭义的终端:
一般有两种: 字符哑终端; 图形终端
字符哑终端,就是自己没有处理能力,只能输入输出的;
图形终端其实他有处理能力,例如framebuffer...
linux中,真正能称得上是终端的其实物理组件就是键盘+显示器,或者称这两个东西叫做控制台
但是linux虚拟出来了6个终端设备
tty1 ~ tty6
而/dev/tty0是一个symbol link指向当前终端,/dev/console则可以为/dev/tty0提供缓冲(这个我不确定)

tty7是图形终端
图形终端中,会用软件类似rxvt虚拟出更多的伪终端,类似我们看到的/dev/pts/0...

伪终端是一种虚拟终端
但是有时候所谓的虚拟终端还是能对应上物理设备

对于伪终端pts和ptm
我自己感觉就像PIPE,只不过输入输出时有终端行规程来控制格式。

后面有讲会话,会话由进程组构成,不过我想不清楚运用场景...

这一章有一些没看懂,进程组,会话,控制终端...因为不怎么用,先放这里,以后再翻。
没有作业...
posted @ 2013-05-31 19:50 Torres 阅读(220) | 评论 (0)编辑 收藏

记录一些基本概念
PID == 0的是Swapper进程(调度进程),这个Process的RO应该是pre load的时候都放到内存里面了,不run任何磁盘上的code,属于系统进程。
PID == 1的是init进程。这个是一个以root运行的用户进程
fork,我有看到一个词fork hope...我觉得不错,我也希望我能fork出hope...
fork的返回状态是有原因的,APUE的解释还不错吧
fork 在 父进程中返回子进程的 PID,因为没有任何API能知道 child process id,而且child可能会有很多。。。
fork 在 子进程中返回0,是因为0是swapper进程,所以没关系;也没必要返回父进程ID,可以通过getppid()得到。
fork后,子进程父进程共享正文 RO
RW ZI Heap Stack是父进程的副本,副本意味着copy
linux已经有很多使用COW了(copy-on-write),这个东西应该是要借助MMU
因为基本fork后会跟exec,那样子所有的东西又都需要马上被替换掉
都设置为Read Only,无论是parent还是child只要一个试图修改,那么就copy,单位应该是MMU的Page
linux有提供clone,这个东西可以由调用者控制什么东西是要和父进程共享的,我还没看到pthread的实现,猜想应该就是通过这东西实现资源共享的。
tips:{
对于字符串求长度,sizeof会计算null,这也正常,因为sizeof()是算memory被分配的大小;strlen计算的是有效字符的长度,不包含null。
如果字符串是常量,sizeof能在编译时被优化,也变成常量,strlen则是函数调用不能被优化。
}
vfork
具体实现我没有具体去想,这个东西也没有什么特别的,跟fork不一样的是,他必须接上exec or exit,否则会run在父进程空间而不做任何复制
所以会有同步,在子进程还未call exec or exit前,父进程会被suspend。要不然父进程受的影响便是未知的。
另外这里也有讨论exit函数在做清理工作时对流的处理
应该是说 一般exit不会去做关闭流的动作,而把这件事情留给kernel,只是flush stream就好,否则类似遇到vfork这种会在父进程空间做事情的,
如果call exit把流给关闭了,会有一些不预期的结果。
父进程提前终止,那么子进程的父进程都会被置为1--> init process.
init process会为每个子进程无论是自己fork出来的还是因为父进程提前终止而领养的做wait获取终止状态,避免zombie进程
处理方式应该是收到SIGCHLD后call wait(null)
wait系列的函数还蛮多的...
对于不想使自己的子进程处于zombie状态的,自己有还想做一些事情的且不想关心子进程结束状态的可以fork两次,exit第一次出来的进程,wait他,然后第二次fork出来的
进程的父进程就变成init了,不过需要使用这种技巧的Case我没想到。。。

关于exec系列函数,我想最好还是记住,因为经常会用,不能每次用都去查吧。
一共6个,exec能给的最多就这三个参数,要run的程序路径;要给run的程序的参数;要给run的程序的环境变量
其中只有execve是 system call,其他都是库函数。
execve(const char* name, char* const argv[], char* const envp[])

这里又有讲三个用户ID的用法
实际用户ID;有效用户ID;保存的设置用户ID

这里有讲一个Case,应该算经典Case,也只有类似Case才能让你明白保存的设置用户ID的用途

man程序为设置用户ID/设置组ID,就会在两种权限中切换。
运行man的时候
real user id == login id
effective user id == man
saved effective user id == man

在man访问完他自己拥有的那些file后,想运行一些命令call setuid(getuid())因为我们不是root
real user id == login id
effective user id == login id
saved effective user id == man
这个时候运行命令就是我们login时候的user的权限

完了后面想设置回去的时候再call setuid(euid),这里euid就是man,之前可以保存在man自己的Context中
这个时候euid == saved effective user id,所以effective user id又被设置为man,这种情况下如果没有saved effective user id那用户权限就回不去了。
real user id == login id
effective user id == man
saved effective user id == man

所以注意setuid 如果是root用户,那么三个ID都会被设置成目标ID,如果不是
则effective user id可能会改变 当目标ID为 real user id or saved effective user id的时候,从设计的角度仔细想,这样子也合理。

system的实现
execel shell,而不是直接去fork/execel本身要system的程序,那样会变得异常复杂,你需要解析cmdString,分离出程序和参数,带入和shell一样的环境变量,找到目的程序,execute他,然后带回结果。
一般会去call system的程序不要使用set-user-id,这样容易导致安全泄露。容易让system出来的那个process权限扩大,不易控制。

贴一个system的实现,非常nice
#include <sys/types.h>
#include 
<signal.h>
#include 
<stdlib.h>
#include 
<unistd.h>
#include 
<paths.h>
#include 
<sys/wait.h>

extern char **environ;

int
system(
const char *command)
{
  pid_t pid;
    sig_t intsave, quitsave;
    sigset_t mask, omask;
    
int pstat;
    
char *argp[] = {"sh""-c", NULL, NULL};

    
if (!command)        /* just checking */
        
return(1);

    argp[
2= (char *)command;

    sigemptyset(
&mask);
    sigaddset(
&mask, SIGCHLD);
    sigprocmask(SIG_BLOCK, 
&mask, &omask);
    
switch (pid = vfork()) {
    
case -1:            /* error */
        sigprocmask(SIG_SETMASK, 
&omask, NULL);
        
return(-1);
    
case 0:                /* child */
        sigprocmask(SIG_SETMASK, 
&omask, NULL);
        execve(_PATH_BSHELL, argp, environ);
    _exit(
127);
  }


    intsave 
= (sig_t)  bsd_signal(SIGINT, SIG_IGN);
    quitsave 
= (sig_t) bsd_signal(SIGQUIT, SIG_IGN);
    pid 
= waitpid(pid, (int *)&pstat, 0);
    sigprocmask(SIG_SETMASK, 
&omask, NULL);
    (
void)bsd_signal(SIGINT, intsave);
    (
void)bsd_signal(SIGQUIT, quitsave);
    
return (pid == -1 ? -1 : pstat);
}

这里使用了vfork和execve,他的实现细节以及可移植性做的还是蛮完美的。

作业:
8.1 如果并非如此那么exit就没有关闭stdout,这在之前也讲过一般的C库都不会这么多此一举,一般他们只会去flush stream不会close
所以如果想达到那种效果,可以自己去显示去fclose

8.2
这个有点复杂啦,vfork后理论上应该立即调用exec才对,如果不去exec or exit,那么子进程就run在父进程的数据空间,如果子进程从另外一个函数返回,不call exit,那么就会一直等到main函数退出,父进程才会执行,但是这个时候stack里面已经没有任何东西了,我不确定会发生什么事情

8.5
不提供返回保存的有效用户ID的函数

8.6 8.7稍后贴上source code.


posted @ 2013-05-31 00:25 Torres 阅读(181) | 评论 (0)编辑 收藏

第六章不看,直接到第七章

所谓的启动例程用C写起来一般是这样,但一般情况这个会是汇编来写
exit(main(argc, argv))

exit函数
_exit和_Exit立即进入kernel,exit则需要做完必要的清理工作,先run注册过的终止处理程序,然后清理没有关闭的流。
exit _Exit是ISO C标准,_exit则是Posix,所以头文件不一样
前面是stdlib.h 后面是unistd.h

关于malloc,calloc,realloc
malloc分配的空间初始值不确定;calloc会帮忙清零;realloc可以动态分配合适的大小空间,如果不够会有copy原始数据的动作,所以对realloc之前的memory记录指针不是好方法,因为realloc完就可能失效。

这三个都是去call sbrk来扩大缩小heap
但一般只是扩大,在free的时候 malloc 会把扩大的 memory cache起来(virual memory)

setjmp, longjmp
跳转函数,支持跨越函数帧的跳转,goto是函数内跳转
注意: longjmp到setjmp的点后,一般情况 全局/局部/局部静态变量都没办法恢复到setjmp时的值;register/volatile变量在编译后(cc -O)会放到寄存器中,这些都能实现回滚。这也就说明setjmp的时候,jmp_buf并没有保存这些值。

7.1 如果不调用return 和 exit 函数返回值用了printf的返回值,Orz...我没想清楚怎么做的。

7.2 取决于printf面对的设备类型,行缓冲的话,碰到换行就会被输出,如果是全缓冲,那么则要么需要call fflush,要么等stream被close。

7.3 不用参数传递,不用全局变量,Orz...要传递argc, argv -->NO

7.4 通常null就是0,一般指针为 null 的时候意味着空指针引用,一般0都不会去放数据,RO的起始位置在一个更高的地址,0会是无效的,用来指引程序出错。

7.5
typedef void func(void);
int atexit(func* fp);
或者
typedef void (*func)(void);
int atexit(func fp);
我喜欢下面那种,因为函数本身就是个pointer。

7.6 calloc会将分配的空间清零,ISO C不保证0值和空指针相等

7.7 size输出的是静态的RO,RW,ZI,像Heap,Stack是runtime才有的。

7.8 理论上不带debug信息的.out文件应该大小都是等于RO+RW+ZI的。

7.9 这就是.a和.so的区别了,link的时候静态库会被直接copy过来,所以会变大很多。

7.10 变量作用域的故事。
posted @ 2013-05-28 20:00 Torres 阅读(154) | 评论 (0)编辑 收藏

     摘要: 这一章会让你想起一些尘封的记忆上大学的时候,教C语言的老师会教大家文件IO,那个时候讲的都是标准输入输出,都是C库的实现,和第三章Unbuffered IO要区别开来,目的之前讲过减少System Call的调用次数,提高PerformanceJava上也有类似的实现,只不过Java的实现会更加Common一些,类似BufferedInputStream/BufferedOutputStream,...  阅读全文
posted @ 2013-05-28 19:24 Torres 阅读(275) | 评论 (0)编辑 收藏

这里要稍微写一些linux下复杂的权限管理,应该只是一小部分知识,还有一些关于cap的东西以后再看。

与process关联的ID
谁execute这个process的人称为real user id,应该就是登陆时使用的user
real group id也一样.
一般在一个登陆Session中,这两个值都不会改变,但是超级用户进程可以改变
像Android中每个APK有独自的User id,然后类似rild之类的native deamon process就会尝试改变自己的user id为类似radio之类的东西。

在运行中检查权限所使用的id称为有效用户id,effective user id
然后还有有效组ID
附加组ID

一般有效用户ID == 真实用户ID, 有效组ID == 真实组ID
但是如果在可执行文件的st_mode中有设置 set-user-ID/set-group-ID
那在执行这个文件的时候 有效用户ID和有效组ID会变成文件的owner

一般有设置set-user-ID/set-group-ID的程序都会获得额外的权限

关于文件权限,有一些容易引起误解的,也还有一些我自己也没有理解清楚的

文件权限比较好理解,O_TRUNC需要文件具有写权限。

文件夹的写权限,应该就是类似能不能在其中create/update/delete文件和文件夹
文件夹的读权限,应该就是读文件夹里面的文件/文件夹列表,通常我们 ls 当前文件夹就必须具有读权限
文件夹的执行权限,这个东西听说又叫搜索位,通常我们cd XXX,就必须在当前文件夹下搜索XXX是否存在,然后就是当我们访问类似这种很长路径的文件/aaa/bbb/ccc/dd.txt
对aaa,bbb,ccc必须具有执行权限,就是搜索

一般如果具有读权限就应该就可以搜索,如果这之前有区别,就是应该是搜索的范围大于能读到的内容。
也就是之后需要知道一个Folder本身里面有写什么内容,我目前知道的是一定有当前的文件列表--> TODO: 看情景分析 or 去看下 ls 的实现,ls里面有比较多参数有些是需要x的有些是需要r的,例如 ls -l如果没有执行权限就只能拿到 name ,能知道是folder还是file,除此之外其他的东西都拿不到,文件本身的权限,user,group

-->linux目录中记录的应该是只有两个东西 inode & name.这与文件系统的实现有关。

另外增删文件都需要WX权限

另外内核对文件操作权限的判断顺序...如果是owner就会看owner权限,group权限就不会看了,如果在group中则others也就不看了,这个逻辑上正常,而且可以想一下kernel的实现,一定是if-else的判断出结果后直接返回。


这里就要提如果新建文件/文件夹的时候文件夹的owner user id和group id是谁
user id会是process 有效用户ID
group id可以有两种选择父folder的group id;or process有效组ID
linux中根据文件系统的不同有的可以在mount fs的时候选择

文件长度: st_size
lseek到文件末尾之后的空间,会照成文件空洞
文件长度会+空洞的大小,但是这些空洞未必会占用磁盘空间。
du 命令可以看磁盘空间大小

符号链接和所谓的硬链接完全是两回事,建议细读4.14,不要去上网search类似我这种自己记录给自己看的blog...:(

这章内容比较多而且杂,主要是stat中的每个参数的意义,需要思考想清楚的东西也比较多,有比较多关于文件操作的System Call

作业开始:
之后只写有点意义的题目,无意义的也没什么可以写的pass
4.1 stat和lstat的区别,stat基本不会关心是否是S_IFLNK(符号链接),应该是看到S_IFLNK会往下去找真正的File,然后拿到属性值
而lstat旁道S_IFLNK则会直接返回。第一次看到这样的函数设计怪怪的,我的初始感觉是反的。我觉得stat是一个common的设计,不会针对不同的File类型来做一些区别。Orz...最后不是。

4.2 umask 777意味着rwxrwxrwx全部变没掉,但是这也没关系,不知道出题人的意思

4.5 目录和符号链接的长度不可能为0,目录创建出来就会包含. 和 ..而且 . 的inode指向自己本身会占磁盘空间,符号链接肯定也是不能为0的,其中有存指向的链接path

4.6 这个有点意思,后面贴代码

4.7 这个看了下答案,没看懂>为什么kernel默认的创建文件赋予的权限“可能会,也可能不会”受unmask的值的影响?

4.8 du df的区别...

4.9 ~ 后面的题大概看了下,不想写了,好多细节...

 


 

posted @ 2013-05-27 16:42 Torres 阅读(236) | 评论 (0)编辑 收藏

看了下第一次作业,那个排版和高亮背景有点瞎,这次能好点就不错了.

第二章是关于POSIX以及XSI的一些标准和limitation,就不做作业了,直接到第三章,没有睡午觉,有些困,不要写错...

先贴一些比较好的思路
(1)
在AF_LOCAL(Posix: AF_UNIX)的Socket编程中,对Client端来说需要connect
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen)
这里需要给serv_addr一个length,这个length是一个真实的地址长度。
当socket为AF_UNIX的Socket时 socket address为sockaddr_un里面存放着地址类型以及socket path,我们需要知道sockaddr_un中真正存放数据的长度。

函数offsetof是不错的选择
这个function的实现看起来可以学习,我先抄一段贴下面

#define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER )

宏功能:获得一个结构体变量成员在此结构体中的偏移量。

1. ( (TYPE *)0 ) 将零转型为TYPE类型指针;
2. ((TYPE *)0)->MEMBER 访问结构中的数据成员;

3. &( ( (TYPE *)0 )->MEMBER )取出数据成员的地址,即相对于0的偏移量,要的就这个;
4.(size_t)(&(((TYPE*)0)->MEMBER))结果转换类型,size_t应该最终为unsigned int类型。
此宏的巧妙之处在于将 0 转换成(TYPE*),这样结构体中成员的地址即为在此结构体中的偏移量。

这里主要还是利用了 '->' 这个符号的功能,编译器解析这个符号的时候应该就是通过偏移量取值,所以这里先定义好取值规则(TYPE*)然后利用偏移量取值反向再取地址,再然后因为起始地址是0,取回的地址就变成了真正的偏移量。
还有一个知道member求整体的Ptr的Marco应该也蛮好用,括号比较多,我没头去看具体实现了...

 

#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr – offsetof(type,member) );})

宏功能:从结构体(type)某成员变量(member)指针(ptr)来求出该结构体(type)的首指针。

(2)
有讨论一些东西,没有搞清楚,贴在这里以后有心力了再看,在看Rild,然后pthread在做线程同步的时候需要先lock,也许大家都是这样做,也一直这样做,但是突然就想到之前Java的时候讨论过的东西为什么wait/notify 一定要在一个synchronize block中,当然你可以讲,如果不会有exception抛出,你也还可以讲wait本来就会先释放lock然后把当前的thread放到Object waiting thread pool中然后挂起当前线程,实现就是这样子,但是从设计来讲,原因我没有想的特别清楚,如果不去lock,是不是因为wait/notify本身有很多操作没办法做multi thread...

然后就是关于select的一些辅助的Marco,类似FD_SET,FD_ZERO之类的,简单讲就是一个long数组,每个元素的每个bit代表一个fd,然后做set,clear之类的动作,最大不能超过那个1024

今天先到这里,写个序,明天正文...Orz

(3)
继续讨论东西
有人为了在C++中作出类似Java可以自动释放内存的东西,也就是所谓的智能指针,Android有帮忙实现strong pointer/weak pointer,他的大概意思就是用一个第三者来看住当前new出来的object,采用的方式一般是引用计数,这种方式也许在Design Pattern中有专门的名字。一般利用编译器对这个第三者做自动释放的时候去检查计数器,看是否能释放他看管的object,C++里面编译器会自动去call ~Constructor就例如局部变量。

有一些复杂的Case,就算是交叉引用了,Strong Pointer/Weak Pointer就是为了解决交叉引用的问题,从语义上讲父Object引用子Object,用SP,反过来用WP
只要SP为0,就可以释放Memory

今天大家有讲Java的WeakReference,SoftReference,我就含糊了,我以为这东西和上面一样。
Java的WeakReference,SoftReference跟上面讲的完全不一样,由于没分清楚,混淆了。JVM就会处理引用交叉的问题,JVM处理引用交叉可以使用类似有向图,如果呈环状而且环中的任意点都没办法到GC Roots(VM stack中的Reference/static reference/JNI reference),那么就都GC掉应该也没问题。
所以Java中的所谓的WeakReference/SoftReference跟SP/WP不一样,Java中普通赋值的reference都是强可及的Strong Reference。
WeakReference只是用来关心下那些将要被回收却没有回收的Object,一旦被GC Thread扫到就会被释放。
而SoftReference是JVM对Object的Cache,只要内存足够不会被释放。

贴一个讲Android SP/WP的链接,之前有印象看过。
http://blog.csdn.net/luoshengyang/article/details/6786239


(4)
Keep Thinking...
某人让我写一个处理字符串的函数,30min, 我怎么也不能让他失望的,是吧~无论11点多的时候我头有多晕,我很开心的在那里写,很直白的讲,我自己用的本是个游戏机,没啥好环境。用C写了个,然后跑出来的结果出乎预料,我想可能是长时间指手画脚惯了~仔细想了下一些文件操作

然后仔细看了一下二进制文件和文本文件,这也是纠正之前的一些疑惑。
基本概念是:
我自己感觉文本文件和二进制文件没啥区别,文本文件就是有明确普通解码方式的显示字符的文件,可以理解说文本文件也是一种二进制文件,只不过这个二进制文件的解码方式是固定好的(ASCII/UTF-8之类)

然后就是读写文本文件时在Windows上有一些转换,写的时候会在\n前面自动加上\r,读的时候反过来,Linux则没有区别。

有一件事情是要明确知道的
像printf之类的东西,帮助我们输出的,其实我们往printf里面塞的是 int 值,这是一个二进制的值,但是我们从stdout中看到的则已经变成了字符,一定有人帮忙做了转换.
我原本以为printf会是检查目标文件,如果目标文件为二进制则直接输出原本的值,如果目标为文本文件就输出对应值的字符

这种理解是错的,不同的输入输出函数带来了不同的结果
类似printf这种格式话输出函数,会把输出内容按照字符以默认的编码方式encode放入文件,所以那个文件里面写的值已经是真实值的一种显示方式(字符)了
然后scanf这种函数在读的之后,也会把字符转换为对应的值,一般ASCII就直接 -0x30了

像read/write这种function他们不会做任何转换,直接就把值放进去了,所以你用文本文件打开的时候,按照默认的decode方式来解析他的时候会发现是乱码。

这个东西与文本文件二进制文件没有关系。
不知道是不是我自己很弱,现在才知道这个东西。

作业终于开始:
3.1 这章主要讲文件Unbuffered IO,这里的unbuffered是指userspace没有buffer来缓冲,kernel一定还是有缓冲的,一般userspace的缓冲做在标准C lib中
另外像Android这种架构Java layer会做缓冲(BufferedInputStream/BufferedOutputStream, 默认是8K)
Java layer的缓冲可以减少 JNI 调用次数
C lib缓冲可以减少 system call 的次数

3.2 自己实现dup2
不能用fcntl那就只能用dup了
这里是想要让做题的人知道,kernel对进程文件描述符的分配原则是 "取最小的没有使用的"
所以用dup实现Mydup(fd1, fd2)
1.fd2 < fd1: close fd2; dup(fd1)得到的值就是fd2
2.fd2 = fd1: 直接return fd1
3.fd2 > fd1: 那就只能无限dup了,直到返回值为fd2,然后close掉 fd1 ~ fd2-1
这里需要对文件描述符表,文件表,inode之类的概念熟悉。

3.3
这里主要是要说文件表项的分配,每次open kernel都会分配新的文件表项,dup的作用就是拿到另外一个文件描述符指向相同的文件表项,但是每支文件只有同一份
inode。
fd1,fd2会指向同一份文件表项
fd1,fd2,fd3的文件表项会指向同一份inode
fcntl 作用于fd1,F_SETFD用来设定 close_on_exec 标志这个东西是在文件描述符中的,所以只会影响fd1
如果是F_SETFL则会影响fd1, fd2文件状态标志都放在文件表中。

3.4
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
if (fd > 2) close(fd);
这个东西的作用应该就是重定向标准输入输出和错误输出到fd.

3.5
没什么重要的,shell命令一定是从左往右解析执行的

3.6
虽然是append方式打开,这个东西是针对写,lseek可以自己选定读取位置。
write函数是否是会每次先lseek到文件尾部?然后再写,这样之前lseek的设置就无效了
小东西验证。

后面贴下代码
append会影响每次write,只要是O_APPEND打开,每次write都会添加到文件末尾。

Done

posted @ 2013-05-22 22:58 Torres 阅读(131) | 评论 (0)编辑 收藏

记性不好,所以作业做起来--从第一章开始,期望有始有终
1.1 对根目录(/)来说,没有parent,所以'.' or '..'就没有区别了,可以用ls -li来看inode,/.. 代表他本身
1.2 多实例,Process ID递增,分配Process ID的具体方式-->//TODO:去查情景分析
1.3 const char* 放到perror中不想让perror自己改,其实我觉得strerror也可以const int
1.4 errno related
比较早之前errno是by process的,这并不合适,Linux支持多线程存取errno
extern int* __errno_location(void)
#define errno (* __errno_location())
Macro为一个函数,使得__errno_location有机会返回一个TSL的variable
这里APUE有Wrap一些简单的打error log的函数,由于errno是一个by thread variable,所以必须先保存
要不然后面print之类的system call同样有可能出现error然后覆盖掉errno
有看到变参函数,纠正了我自己长期的一个误区,之前一直以为变参函数只能用在特定的Case下才有用,例如scanf/printf之类的输入输出函数
因为他们都有带类似format的参数,能够通过%d,%c之类的东西找到后面的变参的个数以及类型,今天有去看一下va_list/va_arg/va_end的实现,会发现变参函数是,本来也应该是一种更加common的设计
这里顺带提一下function call的大体流程,这里需要比较多的背景知识,类似函数调用约定,程序内存分布...
找来一张图比较好看Linux process userspace的布局,印象中windows也一样只不过windows默认是2G/2G,linux是大家都知道的1G/3G


userspace从0~0xC0000000,kernel space(0xC0000000~0xffffffff)的样子跟这个差别比较大,那边分vmalloc/kmalloc还有类似high memory的概念,这里主要贴出userspace,可以看一下RO,RW,ZI,Heap的位置,顺便提一下整个linux virtual memory其实都可以分为两部分: anonymous memory/mapped memory,像RO就是mapped memory,RW开始是mapped memory,只要有人改变初始值,那就会变成anonymous memory,ZI,Heap,Stack这些都是anonymous memory,与函数调用相关的主要是stack -->stack的地址是向低地址增长的,栈底在高地址,栈顶在低地址
然后介绍两种常用的函数调用约定,一种是C语言默认的函数调用约定 __cdecl; 另外一种是PASCAL默认的调用约定 __stdcall
这两种都是从右到左将参数压栈,然后再push ebp; mov ebp, esp; 再然后压入函数局部变量,不一样的是cdecl是由caller function将函数参数出栈,stdcall是由callee function将函数出栈。
这两种方式各有好处,慨括讲一下cdecl的话每个调用者都必须去做pop stack的动作,code size会变大;stdcall则在callee function本身来看会比较单纯
但是这种类型比较难从编译器的角度来支持变参函数。
变参函数都是cdecl
可以玩一下这样的函数...

 

 1#include <cstdlib>
 2#include <iostream>
 3
 4using namespace std;
 5
 6void foo(int cnt, )
 7{
 8    cout << "start foo" << endl; 
 9    int* p = &cnt;
10    for(int i = 0; i < cnt; i++){        
11        cout << *(++p) << endl;
12    }

13    cout << "end foo" << endl;
14}

15
16int main(int argc, char *argv[])
17{
18    int a = 1, b = 3, c = 5, d = 7;
19    void (*functest)(int cnt, );
20    functest = foo;
21    functest(4, a, b, c, d);
22    system("PAUSE");
23    return EXIT_SUCCESS;
24}


加一点对va_list/va_arg/va_end的说法,其实他的实现蛮灵活的

///stdarg.h
#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end  

///vadefs.h
#define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) )
typedef 
char *  va_list;
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap)      ( ap = (va_list)0 )

贴一段网上看到的应该是i386的实现,类似这种变参函数会直接去用指针操作stack来找参数的,因为涉及到对其的问题一定是by platform的,不太好移植
像_INTSIZEOF(n) 就是int对其
大概讲下意思 va_list 就是 char*,是一个用来找参数ptr的游标
va_start(ap, v)  通过第一个固定参数v 初始化 ap
va_arg(ap, t)  这里就给Type t了,所以并不一定需要类似printf的 format 来找,caller和callee可以有一些其他的约定方式,这个返回当前类型为t的参数的值,并且将ap指向下个参数,因为之前ap被初始化的时候其实他并不知道他指向的参数的类型。
va_end(ap) 清掉ap
多说一句,printf系列的函数还蛮多的,fprintf/sprintf/vprintf/svnprintf...带f的是面向FILE streaming的,s是针对char buffer的,v则是说参数是带va_list的,n则是具体指size
1.5/1.6讨论32位表示时间溢出的,不想写答案,时间比较晚,第一章作业写完。

 

记录一些关于系统总线与CPU“位数"的基本概念

通常所说的CPU的位数,32位or64位CPU,指的是ALU(算术逻辑单元)的宽度,也就是这个ALU处理数据的基本单元的宽度
所以数据总线基本会和ALU宽度相同(有例外,这个我没想清楚工作原理) -->应该是可以新加一些Module来做转换。
而地址总线则是CPU寻址的能力,一个是怎么去寻址,一个是寻到地址后,地址中内容的宽度(当然这个宽度跟地址类型(byte,short,int)有关,但送给CPU的时候一般是单位次数送数据总线的宽度的数据),地址总线决定CPU能访问的Memory的范围。

8086是16位ALU 20位数据总线寻址1M
每次CPU送出的地址都是16位,然后加上段寄存器作为最高4位

 

posted @ 2013-05-21 23:25 Torres 阅读(181) | 评论 (0)编辑 收藏

仅列出标题
共7页: 1 2 3 4 5 6 7